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

SetupInterfaceTemplate(main_window[, name, ui])

Base class for the setup interface of a visual interface.

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 sys
 4import time
 5from pathlib import Path
 6
 7import numpy as np
 8from PySide6.QtCore import QByteArray, QMetaObject, QProcess, QTimer, Qt, Signal
 9from PySide6.QtGui import QCloseEvent
10from PySide6.QtNetwork import QHostAddress, QUdpSocket
11from PySide6.QtWidgets import QCheckBox, QPushButton, QWidget
12
13from myogestic.gui.widgets.logger import LoggerLevel
14from myogestic.gui.widgets.templates.visual_interface import SetupInterfaceTemplate
15from myogestic.gui.widgets.visual_interfaces.virtual_hand_interface.ui import (
16    Ui_SetupVirtualHandInterface
17)
18from myogestic.utils.constants import MYOGESTIC_UDP_PORT
19
20# Stylesheets
21NOT_CONNECTED_STYLESHEET = "background-color: red; border-radius: 5px;"
22CONNECTED_STYLESHEET = "background-color: green; border-radius: 5px;"
23
24# Constants
25STREAMING_FREQUENCY = 32
26TIME_BETWEEN_MESSAGES = 1 / STREAMING_FREQUENCY
27
28SOCKET_IP = "127.0.0.1"
29STATUS_REQUEST = "status"
30STATUS_RESPONSE = "active"
31
32
33# Ports
34# on this port the VHI listens for incoming messages from MyoGestic
35VHI__UDP_PORT = 1236
36
37# on this port the VHI sends the currently displayed predicted hand after having applied linear interpolation
38VHI_PREDICTION__UDP_PORT = 1234
39
40
Class Definition#
41class VirtualHandInterface_SetupInterface(SetupInterfaceTemplate):
42    """
43    Setup interface for the Virtual Hand Interface.
44
45    This class is responsible for setting up the Virtual Hand Interface.
46
47    Attributes
48    ----------
49    predicted_hand__signal : Signal
50        Signal that emits the predicted hand data.
51
52    Parameters
53    ----------
54    main_window : QMainWindow
55        The main window of the application.
56    name : str
57        The name of the interface. Default is "VirtualHandInterface".
58
59        .. important:: The name of the interface must be unique.
60    """
61
62    predicted_hand__signal = Signal(np.ndarray)
63
64    def __init__(self, main_window, name="VirtualHandInterface"):
65        super().__init__(main_window, name, ui=Ui_SetupVirtualHandInterface())
66
67        self._unity_process = QProcess()
68        self._unity_process.setProgram(str(self._get_unity_executable()))
69        self._unity_process.started.connect(
70            lambda: self._main_window.toggle_selected_visual_interface(self.name)
71        )
72        self._unity_process.finished.connect(self.interface_was_killed)
73        self._unity_process.finished.connect(
74            lambda: self._main_window.toggle_selected_visual_interface(self.name)
75        )
76
77        self._setup_timers()
78
79        self._last_message_time = time.time()
80        self._is_connected: bool = False
81        self._streaming__udp_socket: QUdpSocket | None = None
82
83        # Custom Stuff
84        self._predicted_hand__udp_socket: QUdpSocket | None = None
85        self._predicted_hand_recording__buffer: list[(float, np.ndarray)] = []
86
87        # Initialize Virtual Hand Interface UI
88        self.initialize_ui_logic()
89
Helper Functions#
 90    @staticmethod
 91    def _get_unity_executable() -> Path:
 92        """Get the path to the Unity executable based on the platform."""
 93        base_dir = (
 94            Path("dist") if not hasattr(sys, "_MEIPASS") else Path(sys._MEIPASS, "dist")
 95        )
 96        unity_executable_paths = {
 97            "Windows": base_dir / "windows" / "Virtual Hand Interface.exe",
 98            "Darwin": base_dir
 99            / "macOS"
100            / "Virtual Hand Interface.app"
101            / "Contents"
102            / "MacOS"
103            / "Virtual Hand Interface",
104            "Linux": base_dir / "linux" / "VirtualHandInterface.x86_64",
105        }
106
107        executable = unity_executable_paths.get(platform.system())
108        if executable and executable.exists():
109            return executable
110        raise FileNotFoundError(
111            f"Unity executable not found for platform {platform.system()}."
112        )
114    def _setup_timers(self):
115        """Setup the timers for the Virtual Hand Interface."""
116        self._status_request__timer = QTimer(self)
117        self._status_request__timer.setInterval(2000)
118        self._status_request__timer.timeout.connect(self.write_status_message)
119
120        self._status_request_timeout__timer = QTimer(self)
121        self._status_request_timeout__timer.setSingleShot(True)
122        self._status_request_timeout__timer.setInterval(1000)
123        self._status_request_timeout__timer.timeout.connect(self._update_status)
UI Setup#
125    def initialize_ui_logic(self):
126        """Initialize the UI logic for the Virtual Hand Interface."""
127        self._main_window.ui.visualInterfacesVerticalLayout.addWidget(self.ui.groupBox)
128
129        self._toggle_virtual_hand_interface__push_button: QPushButton = (
130            self.ui.toggleVirtualHandInterfacePushButton
131        )
132        self._toggle_virtual_hand_interface__push_button.clicked.connect(
133            self.toggle_virtual_hand_interface
134        )
135        self._virtual_hand_interface__status_widget: QWidget = (
136            self.ui.virtualHandInterfaceStatusWidget
137        )
138
139        self._virtual_hand_interface__status_widget.setStyleSheet(
140            NOT_CONNECTED_STYLESHEET
141        )
142
143        self._use_external_virtual_hand_interface__check_box: QCheckBox = (
144            self.ui.useExternalVirtualHandInterfaceCheckBox
145        )

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.#
147    def start_interface(self):
148        """Start the Virtual Hand Interface."""
149        if not self._use_external_virtual_hand_interface__check_box.isChecked():
150            self._unity_process.start()
151            self._unity_process.waitForStarted()
152        self._status_request__timer.start()
153        self.toggle_streaming()
Stop Interface - This method stops the interface.#
155    def stop_interface(self):
156        """Stop the Virtual Hand Interface."""
157        if not self._use_external_virtual_hand_interface__check_box.isChecked():
158            self._unity_process.kill()
159            self._unity_process.waitForFinished()
160        # In case the stop function would be called from outside the main thread we need to use invokeMethod
161        QMetaObject.invokeMethod(self._status_request__timer, "stop", Qt.QueuedConnection)
162        self.toggle_streaming()
Interface Was Killed - This method checks if the interface was killed (e.g. by the user closing the window).#
164    def interface_was_killed(self) -> None:
165        """Handle the case when the Virtual Hand Interface was killed."""
166        self._toggle_virtual_hand_interface__push_button.setChecked(False)
167        self._toggle_virtual_hand_interface__push_button.setText("Open")
168        self._use_external_virtual_hand_interface__check_box.setEnabled(True)
169        self._virtual_hand_interface__status_widget.setStyleSheet(
170            NOT_CONNECTED_STYLESHEET
171        )
172        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”.#
187    def _update_status(self) -> None:
188        """Update the status of the Virtual Hand Interface."""
189        self._is_connected = False
190        self._virtual_hand_interface__status_widget.setStyleSheet(
191            NOT_CONNECTED_STYLESHEET
192        )
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.#
194    def toggle_virtual_hand_interface(self):
195        """Toggle the Virtual Hand Interface."""
196        if self._toggle_virtual_hand_interface__push_button.isChecked():
197            print("Opening Virtual Hand Interface")
198            self.start_interface()
199            self._use_external_virtual_hand_interface__check_box.setEnabled(False)
200            self._toggle_virtual_hand_interface__push_button.setText("Close")
201        else:
202            print("Closing Virtual Hand Interface")
203            self.stop_interface()
204            self._use_external_virtual_hand_interface__check_box.setEnabled(True)
205            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.#
207    def toggle_streaming(self) -> None:
208        """Toggle the streaming of the Virtual Hand Interface."""
209        if self._toggle_virtual_hand_interface__push_button.isChecked():
210            self._streaming__udp_socket = QUdpSocket(self)
211            self._streaming__udp_socket.readyRead.connect(self.read_message)
212            self.outgoing_message_signal.connect(self.write_message)
213            self._streaming__udp_socket.bind(
214                QHostAddress(SOCKET_IP), MYOGESTIC_UDP_PORT
215            )
216
217            self._predicted_hand__udp_socket = QUdpSocket(self)
218            self._predicted_hand__udp_socket.bind(
219                QHostAddress(SOCKET_IP), VHI_PREDICTION__UDP_PORT
220            )
221            self._predicted_hand__udp_socket.readyRead.connect(self.read_predicted_hand)
222
223            self._last_message_time = time.time()
224        else:
225            try:
226                self._streaming__udp_socket.close()
227                self._predicted_hand__udp_socket.close()
228            except AttributeError:
229                pass
230            self._streaming__udp_socket = None
231            self._predicted_hand__udp_socket = None
232            self._is_connected = False
233            self._virtual_hand_interface__status_widget.setStyleSheet(
234                NOT_CONNECTED_STYLESHEET
235            )
Read Predicted Hand - This method reads the predicted hand from MyoGestic.#
237    def read_predicted_hand(self) -> None:
238        """Read the predicted hand data from the Virtual Hand Interface."""
239        while self._predicted_hand__udp_socket.hasPendingDatagrams():
240            datagram, _, _ = self._predicted_hand__udp_socket.readDatagram(
241                self._predicted_hand__udp_socket.pendingDatagramSize()
242            )
243
244            data = datagram.data().decode("utf-8")
245            if not data:
246                return
247
248            self.predicted_hand__signal.emit(np.array(ast.literal_eval(data)))
Read/Write Message - This methods read and write messages from/to MyoGestic.#
266    def read_message(self) -> None:
267        """Read a message from the Virtual Hand Interface."""
268        if self._toggle_virtual_hand_interface__push_button.isChecked():
269            while self._streaming__udp_socket.hasPendingDatagrams():
270                datagram, _, _ = self._streaming__udp_socket.readDatagram(
271                    self._streaming__udp_socket.pendingDatagramSize()
272                )
273
274                data = datagram.data().decode("utf-8")
275                if not data:
276                    return
277
278                try:
279                    if data == STATUS_RESPONSE:
280                        self._is_connected = True
281                        self._virtual_hand_interface__status_widget.setStyleSheet(
282                            CONNECTED_STYLESHEET
283                        )
284                        self._status_request_timeout__timer.stop()
285                        return
286
287                    self.incoming_message_signal.emit(np.array(ast.literal_eval(data)))
288                except (UnicodeDecodeError, SyntaxError):
289                    pass
250    def write_message(self, message: QByteArray) -> None:
251        """Write a message to the Virtual Hand Interface."""
252        if self._is_connected and (
253            time.time() - self._last_message_time >= TIME_BETWEEN_MESSAGES
254        ):
255            self._last_message_time = time.time()
256            output_bytes = self._streaming__udp_socket.writeDatagram(
257                message, QHostAddress(SOCKET_IP), VHI__UDP_PORT
258            )
259
260            if output_bytes == -1:
261                self._main_window.logger.print(
262                    "Error in sending message to Virtual Hand Interface!",
263                    level=LoggerLevel.ERROR,
264                )
291    def write_status_message(self) -> None:
292        """Write a status message to the Virtual Hand Interface."""
293        if self._toggle_virtual_hand_interface__push_button.isChecked():
294            output_bytes = self._streaming__udp_socket.writeDatagram(
295                STATUS_REQUEST.encode("utf-8"),
296                QHostAddress(SOCKET_IP),
297                VHI__UDP_PORT,
298            )
299
300            if output_bytes == -1:
301                self._main_window.logger.print(
302                    "Error in sending status message to Virtual Hand Interface!",
303                    level=LoggerLevel.ERROR,
304                )
305                return
306
307            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.#
309    def connect_custom_signals(self) -> None:
310        """Connect custom signals for the Virtual Hand Interface."""
311        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.#
332    def online_predicted_hand_update(self, data: np.ndarray) -> None:
333        """Update the predicted hand data for the online protocol."""
334        if self._online_protocol.online_record_toggle_push_button.isChecked():
335            self._predicted_hand_recording__buffer.append(
336                (time.time() - self._online_protocol.recording_start_time, data)
337            )
Clear Custom Signal Buffers - This method clears the custom signal buffers.#
328    def clear_custom_signal_buffers(self) -> None:
329        """Clear custom signal buffers for the Virtual Hand Interface."""
330        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.#
317    def get_custom_save_data(self) -> dict:
318        """Get custom save data for the Virtual Hand Interface."""
319        return {
320            "predicted_hand": np.vstack(
321                [data for _, data in self._predicted_hand_recording__buffer],
322            ).T,
323            "predicted_hand_timings": np.array(
324                [time for time, _ in self._predicted_hand_recording__buffer],
325            ),
326        }

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).
)

Total running time of the script: (0 minutes 0.259 seconds)

Estimated memory usage: 528 MB

Gallery generated by Sphinx-Gallery