Note
Go to the end to download the full example code.
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:
|
Base class for the setup interface of a visual interface. |
The base class provides:
outgoing_message_signal(Signal(QByteArray)) – Send data to the VI.incoming_message_signal(Signal(np.ndarray)) – Receive data from the VI._main_window– Reference to the main MyoGestic application._record_protocol,_training_protocol,_online_protocol– Protocol references.
You must implement these abstract methods:
initialize_ui_logic()– Wire up UI widgets.start_interface()– Launch the VI process.stop_interface()– Stop the VI process.interface_was_killed()– Handle unexpected VI termination.close_event()– Clean up on application exit.connect_custom_signals()/disconnect_custom_signals()– Connect/disconnect data streaming signals.clear_custom_signal_buffers()– Reset internal data buffers.
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.
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
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#
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()
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#
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 )
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#
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)
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,
)