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 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.
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
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
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)
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.
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()
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
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 )
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.
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 )
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)))
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.
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)
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 )
392 def clear_custom_signal_buffers(self) -> None:
393 """Clear custom signal buffers for the Virtual Hand Interface."""
394 self._predicted_hand_recording__buffer = []
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).
)