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