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