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_dirs = [
 94            Path("dist") if not hasattr(sys, "_MEIPASS") else Path(sys._MEIPASS, "dist"),
 95            Path("myogestic", "dist") if not hasattr(sys, "_MEIPASS") else Path(sys._MEIPASS, "dist"),
 96        ]
 97
 98        unity_executable_paths = {
 99            "Windows": "windows/Virtual Hand Interface.exe",
100            "Darwin": "macOS/Virtual Hand Interface.app/Contents/MacOS/Virtual Hand Interface",
101        "Linux": "linux/VirtualHandInterface.x86_64",
102    }
103
104        for base_dir in base_dirs:
105            executable = base_dir / unity_executable_paths.get(platform.system(), "")
106            if executable.exists():
107                return executable
108
109        raise FileNotFoundError(f"Unity executable not found for platform {platform.system()}.")
111    def _setup_timers(self):
112        """Setup the timers for the Virtual Hand Interface."""
113        self._status_request__timer = QTimer(self)
114        self._status_request__timer.setInterval(2000)
115        self._status_request__timer.timeout.connect(self.write_status_message)
116
117        self._status_request_timeout__timer = QTimer(self)
118        self._status_request_timeout__timer.setSingleShot(True)
119        self._status_request_timeout__timer.setInterval(1000)
120        self._status_request_timeout__timer.timeout.connect(self._update_status)
UI Setup#
122    def initialize_ui_logic(self):
123        """Initialize the UI logic for the Virtual Hand Interface."""
124        self._main_window.ui.visualInterfacesVerticalLayout.addWidget(self.ui.groupBox)
125
126        self._toggle_virtual_hand_interface__push_button: QPushButton = (
127            self.ui.toggleVirtualHandInterfacePushButton
128        )
129        self._toggle_virtual_hand_interface__push_button.clicked.connect(
130            self.toggle_virtual_hand_interface
131        )
132        self._virtual_hand_interface__status_widget: QWidget = (
133            self.ui.virtualHandInterfaceStatusWidget
134        )
135
136        self._virtual_hand_interface__status_widget.setStyleSheet(
137            NOT_CONNECTED_STYLESHEET
138        )
139
140        self._use_external_virtual_hand_interface__check_box: QCheckBox = (
141            self.ui.useExternalVirtualHandInterfaceCheckBox
142        )

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

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

Estimated memory usage: 623 MB

Gallery generated by Sphinx-Gallery