biosignal-device-interface 0.2.1a2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/PKG-INFO +3 -2
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/device_template_widgets/core/base_multiple_devices_widget.py +17 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_plus_widget.py +16 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_widget.py +16 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/device_template_widgets/otb/otb_quattrocento_light_widget.py +7 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/device_template_widgets/otb/otb_quattrocento_widget.py +31 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/device_template_widgets/otb/otb_syncstation_widget.py +7 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/plot_widgets/biosignal_plot_widget.py +54 -6
- biosignal_device_interface-0.2.3/biosignal_device_interface/gui/ui/devices_template_widget.ui +133 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/ui/otb_muovi_plus_template_widget.ui +7 -1
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/ui/otb_muovi_template_widget.ui +7 -1
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/ui/otb_quattrocento_light_template_widget.ui +7 -1
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/ui/otb_quattrocento_template_widget.ui +13 -1
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/ui/otb_syncstation_template_widget.ui +24 -18
- biosignal_device_interface-0.2.3/biosignal_device_interface/gui/ui_compiled/devices_template_widget.py +104 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/ui_compiled/otb_muovi_plus_template_widget.py +5 -4
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/ui_compiled/otb_muovi_template_widget.py +5 -4
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/ui_compiled/otb_quattrocento_light_template_widget.py +6 -5
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/ui_compiled/otb_quattrocento_template_widget.py +4 -2
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/ui_compiled/otb_syncstation_template_widget.py +20 -19
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/pyproject.toml +2 -2
- biosignal_device_interface-0.2.1a2/biosignal_device_interface/gui/ui/devices_template_widget.ui +0 -38
- biosignal_device_interface-0.2.1a2/biosignal_device_interface/gui/ui_compiled/devices_template_widget.py +0 -56
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/LICENSE +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/README.md +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/__init__.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/constants/__init__.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/constants/devices/__init__.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/constants/devices/core/base_device_constants.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/constants/devices/otb/otb_constants.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/constants/devices/otb/otb_muovi_constants.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/constants/devices/otb/otb_quattrocento_constants.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/constants/devices/otb/otb_quattrocento_light_constants.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/constants/devices/otb/otb_syncstation_constants.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/constants/plots/color_palette.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/devices/__init__.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/devices/core/__init__.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/devices/core/base_device.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/devices/otb/__init__.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/devices/otb/otb_muovi.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/devices/otb/otb_quattrocento.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/devices/otb/otb_quattrocento_light.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/devices/otb/otb_syncstation.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/device_template_widgets/__init__.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/device_template_widgets/all_devices_widget.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/device_template_widgets/core/base_device_widget.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/device_template_widgets/otb/__init__.py +0 -0
- {biosignal_device_interface-0.2.1a2 → biosignal_device_interface-0.2.3}/biosignal_device_interface/gui/device_template_widgets/otb/otb_devices_widget.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: biosignal-device-interface
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Python communication interface to many biosignal devices manufactured by several companies to easy integration in custom PySide6 applications.
|
|
5
5
|
License: CC BY-SA 4.0
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: Dominik I. Braun
|
|
7
8
|
Author-email: dome.braun@fau.de
|
|
8
9
|
Requires-Python: >=3.10,<3.13
|
|
@@ -50,6 +50,23 @@ class BaseMultipleDevicesWidget(QWidget):
|
|
|
50
50
|
self.device_stacked_widget = self.ui.deviceStackedWidget
|
|
51
51
|
self.device_selection_combo_box = self.ui.deviceSelectionComboBox
|
|
52
52
|
|
|
53
|
+
# Sync external scrollbar with scroll area's internal scrollbar
|
|
54
|
+
internal_scrollbar = self.ui.deviceScrollArea.verticalScrollBar()
|
|
55
|
+
external_scrollbar = self.ui.deviceScrollBar
|
|
56
|
+
|
|
57
|
+
# Sync scrollbar values bidirectionally
|
|
58
|
+
internal_scrollbar.valueChanged.connect(external_scrollbar.setValue)
|
|
59
|
+
external_scrollbar.valueChanged.connect(internal_scrollbar.setValue)
|
|
60
|
+
|
|
61
|
+
# Sync scrollbar range and visibility when content changes
|
|
62
|
+
def update_scrollbar_range(min_val: int, max_val: int):
|
|
63
|
+
external_scrollbar.setRange(min_val, max_val)
|
|
64
|
+
external_scrollbar.setVisible(max_val > min_val)
|
|
65
|
+
|
|
66
|
+
internal_scrollbar.rangeChanged.connect(update_scrollbar_range)
|
|
67
|
+
# Initialize visibility
|
|
68
|
+
external_scrollbar.setVisible(internal_scrollbar.maximum() > internal_scrollbar.minimum())
|
|
69
|
+
|
|
53
70
|
def get_device_information(self) -> Dict[str, Union[str, int]]:
|
|
54
71
|
return self._get_current_widget().get_device_information()
|
|
55
72
|
|
|
@@ -50,12 +50,15 @@ class OTBMuoviPlusWidget(BaseDeviceWidget):
|
|
|
50
50
|
self.connect_push_button.setText("Disconnect")
|
|
51
51
|
self.connect_push_button.setChecked(True)
|
|
52
52
|
self.configure_push_button.setEnabled(True)
|
|
53
|
+
self.configure_push_button.setToolTip("Step 2: Configure device settings")
|
|
53
54
|
self.connection_group_box.setEnabled(False)
|
|
54
55
|
else:
|
|
55
56
|
self.connect_push_button.setText("Connect")
|
|
56
57
|
self.connect_push_button.setChecked(False)
|
|
57
58
|
self.configure_push_button.setEnabled(False)
|
|
59
|
+
self.configure_push_button.setToolTip("Step 2: Configure device settings (connect first)")
|
|
58
60
|
self.stream_push_button.setEnabled(False)
|
|
61
|
+
self.stream_push_button.setToolTip("Step 3: Start data streaming (configure first)")
|
|
59
62
|
self.connection_group_box.setEnabled(True)
|
|
60
63
|
|
|
61
64
|
self.connect_toggled.emit(is_connected)
|
|
@@ -73,6 +76,7 @@ class OTBMuoviPlusWidget(BaseDeviceWidget):
|
|
|
73
76
|
def _configuration_toggled(self, is_configured: bool) -> None:
|
|
74
77
|
if is_configured:
|
|
75
78
|
self.stream_push_button.setEnabled(True)
|
|
79
|
+
self.stream_push_button.setToolTip("Step 3: Start data streaming")
|
|
76
80
|
|
|
77
81
|
self.configure_toggled.emit(is_configured)
|
|
78
82
|
|
|
@@ -112,16 +116,19 @@ class OTBMuoviPlusWidget(BaseDeviceWidget):
|
|
|
112
116
|
# Command Push Buttons
|
|
113
117
|
self.connect_push_button: QPushButton = self.ui.commandConnectionPushButton
|
|
114
118
|
self.connect_push_button.clicked.connect(self._toggle_connection)
|
|
119
|
+
self.connect_push_button.setToolTip("Step 1: Connect to the Muovi+ device")
|
|
115
120
|
self._device.connect_toggled.connect(self._connection_toggled)
|
|
116
121
|
|
|
117
122
|
self.configure_push_button: QPushButton = self.ui.commandConfigurationPushButton
|
|
118
123
|
self.configure_push_button.clicked.connect(self._toggle_configuration)
|
|
119
124
|
self.configure_push_button.setEnabled(False)
|
|
125
|
+
self.configure_push_button.setToolTip("Step 2: Configure device settings (connect first)")
|
|
120
126
|
self._device.configure_toggled.connect(self._configuration_toggled)
|
|
121
127
|
|
|
122
128
|
self.stream_push_button: QPushButton = self.ui.commandStreamPushButton
|
|
123
129
|
self.stream_push_button.clicked.connect(self._toggle_stream)
|
|
124
130
|
self.stream_push_button.setEnabled(False)
|
|
131
|
+
self.stream_push_button.setToolTip("Step 3: Start data streaming (configure first)")
|
|
125
132
|
self._device.stream_toggled.connect(self._stream_toggled)
|
|
126
133
|
|
|
127
134
|
# Connection parameters
|
|
@@ -148,9 +155,18 @@ class OTBMuoviPlusWidget(BaseDeviceWidget):
|
|
|
148
155
|
# Input parameters
|
|
149
156
|
self.input_parameters_group_box: QGroupBox = self.ui.inputGroupBox
|
|
150
157
|
self.input_working_mode_combo_box: QComboBox = self.ui.inputWorkingModeComboBox
|
|
158
|
+
self.input_working_mode_combo_box.setToolTip(
|
|
159
|
+
"EMG: Electromyography for muscle signals\nEEG: Electroencephalography for brain signals"
|
|
160
|
+
)
|
|
151
161
|
self.input_detection_mode_combo_box: QComboBox = (
|
|
152
162
|
self.ui.inputDetectionModeComboBox
|
|
153
163
|
)
|
|
164
|
+
self.input_detection_mode_combo_box.setToolTip(
|
|
165
|
+
"High Gain: Better for weak signals\nLow Gain: Better for strong signals\nImpedance Check: Verify electrode contact"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Add tooltip for update button
|
|
169
|
+
self.connection_update_push_button.setToolTip("Refresh available device IP addresses")
|
|
154
170
|
|
|
155
171
|
# Configuration parameters
|
|
156
172
|
self.configuration_group_boxes: list[QGroupBox] = [
|
|
@@ -50,12 +50,15 @@ class OTBMuoviWidget(BaseDeviceWidget):
|
|
|
50
50
|
self.connect_push_button.setText("Disconnect")
|
|
51
51
|
self.connect_push_button.setChecked(True)
|
|
52
52
|
self.configure_push_button.setEnabled(True)
|
|
53
|
+
self.configure_push_button.setToolTip("Step 2: Configure device settings")
|
|
53
54
|
self.connection_group_box.setEnabled(False)
|
|
54
55
|
else:
|
|
55
56
|
self.connect_push_button.setText("Connect")
|
|
56
57
|
self.connect_push_button.setChecked(False)
|
|
57
58
|
self.configure_push_button.setEnabled(False)
|
|
59
|
+
self.configure_push_button.setToolTip("Step 2: Configure device settings (connect first)")
|
|
58
60
|
self.stream_push_button.setEnabled(False)
|
|
61
|
+
self.stream_push_button.setToolTip("Step 3: Start data streaming (configure first)")
|
|
59
62
|
self.connection_group_box.setEnabled(True)
|
|
60
63
|
|
|
61
64
|
self.connect_toggled.emit(is_connected)
|
|
@@ -73,6 +76,7 @@ class OTBMuoviWidget(BaseDeviceWidget):
|
|
|
73
76
|
def _configuration_toggled(self, is_configured: bool) -> None:
|
|
74
77
|
if is_configured:
|
|
75
78
|
self.stream_push_button.setEnabled(True)
|
|
79
|
+
self.stream_push_button.setToolTip("Step 3: Start data streaming")
|
|
76
80
|
|
|
77
81
|
self.configure_toggled.emit(is_configured)
|
|
78
82
|
|
|
@@ -112,16 +116,19 @@ class OTBMuoviWidget(BaseDeviceWidget):
|
|
|
112
116
|
# Command Push Buttons
|
|
113
117
|
self.connect_push_button: QPushButton = self.ui.commandConnectionPushButton
|
|
114
118
|
self.connect_push_button.clicked.connect(self._toggle_connection)
|
|
119
|
+
self.connect_push_button.setToolTip("Step 1: Connect to the Muovi device")
|
|
115
120
|
self._device.connect_toggled.connect(self._connection_toggled)
|
|
116
121
|
|
|
117
122
|
self.configure_push_button: QPushButton = self.ui.commandConfigurationPushButton
|
|
118
123
|
self.configure_push_button.clicked.connect(self._toggle_configuration)
|
|
119
124
|
self.configure_push_button.setEnabled(False)
|
|
125
|
+
self.configure_push_button.setToolTip("Step 2: Configure device settings (connect first)")
|
|
120
126
|
self._device.configure_toggled.connect(self._configuration_toggled)
|
|
121
127
|
|
|
122
128
|
self.stream_push_button: QPushButton = self.ui.commandStreamPushButton
|
|
123
129
|
self.stream_push_button.clicked.connect(self._toggle_stream)
|
|
124
130
|
self.stream_push_button.setEnabled(False)
|
|
131
|
+
self.stream_push_button.setToolTip("Step 3: Start data streaming (configure first)")
|
|
125
132
|
self._device.stream_toggled.connect(self._stream_toggled)
|
|
126
133
|
|
|
127
134
|
# Connection parameters
|
|
@@ -148,9 +155,18 @@ class OTBMuoviWidget(BaseDeviceWidget):
|
|
|
148
155
|
# Input parameters
|
|
149
156
|
self.input_parameters_group_box: QGroupBox = self.ui.inputGroupBox
|
|
150
157
|
self.input_working_mode_combo_box: QComboBox = self.ui.inputWorkingModeComboBox
|
|
158
|
+
self.input_working_mode_combo_box.setToolTip(
|
|
159
|
+
"EMG: Electromyography for muscle signals\nEEG: Electroencephalography for brain signals"
|
|
160
|
+
)
|
|
151
161
|
self.input_detection_mode_combo_box: QComboBox = (
|
|
152
162
|
self.ui.inputDetectionModeComboBox
|
|
153
163
|
)
|
|
164
|
+
self.input_detection_mode_combo_box.setToolTip(
|
|
165
|
+
"High Gain: Better for weak signals\nLow Gain: Better for strong signals\nImpedance Check: Verify electrode contact"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Add tooltip for update button
|
|
169
|
+
self.connection_update_push_button.setToolTip("Refresh available device IP addresses")
|
|
154
170
|
|
|
155
171
|
# Configuration parameters
|
|
156
172
|
self.configuration_group_boxes: list[QGroupBox] = [
|
|
@@ -48,12 +48,15 @@ class OTBQuattrocentoLightWidget(BaseDeviceWidget):
|
|
|
48
48
|
self._connect_push_button.setText("Disconnect")
|
|
49
49
|
self._connect_push_button.setChecked(True)
|
|
50
50
|
self._configure_push_button.setEnabled(True)
|
|
51
|
+
self._configure_push_button.setToolTip("Step 2: Configure device settings")
|
|
51
52
|
self._connection_group_box.setEnabled(False)
|
|
52
53
|
else:
|
|
53
54
|
self._connect_push_button.setText("Connect")
|
|
54
55
|
self._connect_push_button.setChecked(False)
|
|
55
56
|
self._configure_push_button.setEnabled(False)
|
|
57
|
+
self._configure_push_button.setToolTip("Step 2: Configure device settings (connect first)")
|
|
56
58
|
self._stream_push_button.setEnabled(False)
|
|
59
|
+
self._stream_push_button.setToolTip("Step 3: Start data streaming (configure first)")
|
|
57
60
|
self._connection_group_box.setEnabled(True)
|
|
58
61
|
|
|
59
62
|
self.connect_toggled.emit(is_connected)
|
|
@@ -81,6 +84,7 @@ class OTBQuattrocentoLightWidget(BaseDeviceWidget):
|
|
|
81
84
|
def _configuration_toggled(self, is_configured: bool) -> None:
|
|
82
85
|
if is_configured:
|
|
83
86
|
self._stream_push_button.setEnabled(True)
|
|
87
|
+
self._stream_push_button.setToolTip("Step 3: Start data streaming")
|
|
84
88
|
|
|
85
89
|
self.configure_toggled.emit(is_configured)
|
|
86
90
|
|
|
@@ -121,6 +125,7 @@ class OTBQuattrocentoLightWidget(BaseDeviceWidget):
|
|
|
121
125
|
# Command Push Buttons
|
|
122
126
|
self._connect_push_button: QPushButton = self.ui.commandConnectionPushButton
|
|
123
127
|
self._connect_push_button.clicked.connect(self._toggle_connection)
|
|
128
|
+
self._connect_push_button.setToolTip("Step 1: Connect to the Quattrocento Light device")
|
|
124
129
|
self._device.connect_toggled.connect(self._connection_toggled)
|
|
125
130
|
|
|
126
131
|
self._configure_push_button: QPushButton = (
|
|
@@ -128,11 +133,13 @@ class OTBQuattrocentoLightWidget(BaseDeviceWidget):
|
|
|
128
133
|
)
|
|
129
134
|
self._configure_push_button.clicked.connect(self._toggle_configuration)
|
|
130
135
|
self._configure_push_button.setEnabled(False)
|
|
136
|
+
self._configure_push_button.setToolTip("Step 2: Configure device settings (connect first)")
|
|
131
137
|
self._device.configure_toggled.connect(self._configuration_toggled)
|
|
132
138
|
|
|
133
139
|
self._stream_push_button: QPushButton = self.ui.commandStreamPushButton
|
|
134
140
|
self._stream_push_button.clicked.connect(self._toggle_stream)
|
|
135
141
|
self._stream_push_button.setEnabled(False)
|
|
142
|
+
self._stream_push_button.setToolTip("Step 3: Start data streaming (configure first)")
|
|
136
143
|
self._device.stream_toggled.connect(self._stream_toggled)
|
|
137
144
|
|
|
138
145
|
# Connection parameters
|
|
@@ -70,12 +70,15 @@ class OTBQuattrocentoWidget(BaseDeviceWidget):
|
|
|
70
70
|
self._connect_push_button.setText("Disconnect")
|
|
71
71
|
self._connect_push_button.setChecked(True)
|
|
72
72
|
self._configure_push_button.setEnabled(True)
|
|
73
|
+
self._configure_push_button.setToolTip("Step 2: Configure device settings")
|
|
73
74
|
self._connection_group_box.setEnabled(False)
|
|
74
75
|
else:
|
|
75
76
|
self._connect_push_button.setText("Connect")
|
|
76
77
|
self._connect_push_button.setChecked(False)
|
|
77
78
|
self._configure_push_button.setEnabled(False)
|
|
79
|
+
self._configure_push_button.setToolTip("Step 2: Configure device settings (connect first)")
|
|
78
80
|
self._stream_push_button.setEnabled(False)
|
|
81
|
+
self._stream_push_button.setToolTip("Step 3: Start data streaming (configure first)")
|
|
79
82
|
self._connection_group_box.setEnabled(True)
|
|
80
83
|
|
|
81
84
|
self.connect_toggled.emit(is_connected)
|
|
@@ -131,6 +134,7 @@ class OTBQuattrocentoWidget(BaseDeviceWidget):
|
|
|
131
134
|
def _configuration_toggled(self, is_configured: bool) -> None:
|
|
132
135
|
if is_configured:
|
|
133
136
|
self._stream_push_button.setEnabled(True)
|
|
137
|
+
self._stream_push_button.setToolTip("Step 3: Start data streaming")
|
|
134
138
|
|
|
135
139
|
self.configure_toggled.emit(is_configured)
|
|
136
140
|
|
|
@@ -174,6 +178,7 @@ class OTBQuattrocentoWidget(BaseDeviceWidget):
|
|
|
174
178
|
# Command Push Buttons
|
|
175
179
|
self._connect_push_button: QPushButton = self.ui.commandConnectionPushButton
|
|
176
180
|
self._connect_push_button.clicked.connect(self._toggle_connection)
|
|
181
|
+
self._connect_push_button.setToolTip("Step 1: Connect to the Quattrocento device")
|
|
177
182
|
self._device.connect_toggled.connect(self._connection_toggled)
|
|
178
183
|
|
|
179
184
|
self._configure_push_button: QPushButton = (
|
|
@@ -181,11 +186,13 @@ class OTBQuattrocentoWidget(BaseDeviceWidget):
|
|
|
181
186
|
)
|
|
182
187
|
self._configure_push_button.clicked.connect(self._toggle_configuration)
|
|
183
188
|
self._configure_push_button.setEnabled(False)
|
|
189
|
+
self._configure_push_button.setToolTip("Step 2: Configure device settings (connect first)")
|
|
184
190
|
self._device.configure_toggled.connect(self._configuration_toggled)
|
|
185
191
|
|
|
186
192
|
self._stream_push_button: QPushButton = self.ui.commandStreamPushButton
|
|
187
193
|
self._stream_push_button.clicked.connect(self._toggle_stream)
|
|
188
194
|
self._stream_push_button.setEnabled(False)
|
|
195
|
+
self._stream_push_button.setToolTip("Step 3: Start data streaming (configure first)")
|
|
189
196
|
self._device.stream_toggled.connect(self._stream_toggled)
|
|
190
197
|
|
|
191
198
|
# Connection parameters
|
|
@@ -214,18 +221,33 @@ class OTBQuattrocentoWidget(BaseDeviceWidget):
|
|
|
214
221
|
self._acquisition_sampling_frequency_combo_box: QComboBox = (
|
|
215
222
|
self.ui.acquisitionSamplingFrequencyComboBox
|
|
216
223
|
)
|
|
224
|
+
self._acquisition_sampling_frequency_combo_box.setToolTip(
|
|
225
|
+
"Higher frequencies capture more detail but generate more data"
|
|
226
|
+
)
|
|
217
227
|
self._acquisition_number_of_channels_combo_box: QComboBox = (
|
|
218
228
|
self.ui.acquisitionNumberOfChannelsComboBox
|
|
219
229
|
)
|
|
230
|
+
self._acquisition_number_of_channels_combo_box.setToolTip(
|
|
231
|
+
"Number of EMG channels to record from"
|
|
232
|
+
)
|
|
220
233
|
self._acquisition_decimator_check_box: QCheckBox = (
|
|
221
234
|
self.ui.acquisitionDecimatorCheckBox
|
|
222
235
|
)
|
|
236
|
+
self._acquisition_decimator_check_box.setToolTip(
|
|
237
|
+
"Reduce sampling rate by averaging samples (reduces data size)"
|
|
238
|
+
)
|
|
223
239
|
self._acquisition_recording_check_box: QCheckBox = (
|
|
224
240
|
self.ui.acquisitionRecordingCheckBox
|
|
225
241
|
)
|
|
242
|
+
self._acquisition_recording_check_box.setToolTip(
|
|
243
|
+
"Enable on-device recording to SD card"
|
|
244
|
+
)
|
|
226
245
|
|
|
227
246
|
# Grid selection
|
|
228
247
|
self._grid_selection_group_box: QGroupBox = self.ui.gridSelectionGroupBox
|
|
248
|
+
self._grid_selection_group_box.setToolTip(
|
|
249
|
+
"Select which electrode grids to use for recording"
|
|
250
|
+
)
|
|
229
251
|
self._grid_selection_check_box_list: list[QCheckBox] = [
|
|
230
252
|
self.ui.gridOneCheckBox,
|
|
231
253
|
self.ui.gridTwoCheckBox,
|
|
@@ -244,14 +266,23 @@ class OTBQuattrocentoWidget(BaseDeviceWidget):
|
|
|
244
266
|
self._input_group_box: QGroupBox = self.ui.inputGroupBox
|
|
245
267
|
self._input_channel_combo_box: QComboBox = self.ui.inputChannelComboBox
|
|
246
268
|
self._input_low_pass_filter_combo_box: QComboBox = self.ui.inputLowPassComboBox
|
|
269
|
+
self._input_low_pass_filter_combo_box.setToolTip(
|
|
270
|
+
"Low-pass filter removes high-frequency noise"
|
|
271
|
+
)
|
|
247
272
|
self._input_low_pass_default = self.ui.inputLowPassComboBox.currentIndex()
|
|
248
273
|
self._input_high_pass_filter_combo_box: QComboBox = (
|
|
249
274
|
self.ui.inputHighPassComboBox
|
|
250
275
|
)
|
|
276
|
+
self._input_high_pass_filter_combo_box.setToolTip(
|
|
277
|
+
"High-pass filter removes low-frequency drift and motion artifacts"
|
|
278
|
+
)
|
|
251
279
|
self._input_high_pass_default = self.ui.inputHighPassComboBox.currentIndex()
|
|
252
280
|
self._input_detection_mode_combo_box: QComboBox = (
|
|
253
281
|
self.ui.inputDetectionModeComboBox
|
|
254
282
|
)
|
|
283
|
+
self._input_detection_mode_combo_box.setToolTip(
|
|
284
|
+
"Monopolar: Each electrode vs reference\nDifferential: Difference between adjacent electrodes"
|
|
285
|
+
)
|
|
255
286
|
|
|
256
287
|
self._configuration_group_boxes: list[QGroupBox] = [
|
|
257
288
|
self._acquisition_group_box,
|
|
@@ -59,12 +59,15 @@ class OTBSyncStationWidget(BaseDeviceWidget):
|
|
|
59
59
|
self._command_connect_push_button.setText("Disconnect")
|
|
60
60
|
self._command_connect_push_button.setChecked(True)
|
|
61
61
|
self._command_configure_push_button.setEnabled(True)
|
|
62
|
+
self._command_configure_push_button.setToolTip("Step 2: Configure device settings")
|
|
62
63
|
self._connection_group_box.setEnabled(False)
|
|
63
64
|
else:
|
|
64
65
|
self._command_connect_push_button.setText("Connect")
|
|
65
66
|
self._command_connect_push_button.setChecked(False)
|
|
66
67
|
self._command_configure_push_button.setEnabled(False)
|
|
68
|
+
self._command_configure_push_button.setToolTip("Step 2: Configure device settings (connect first)")
|
|
67
69
|
self._command_stream_push_button.setEnabled(False)
|
|
70
|
+
self._command_stream_push_button.setToolTip("Step 3: Start data streaming (configure first)")
|
|
68
71
|
self._connection_group_box.setEnabled(True)
|
|
69
72
|
|
|
70
73
|
self.connect_toggled.emit(is_connected)
|
|
@@ -94,6 +97,7 @@ class OTBSyncStationWidget(BaseDeviceWidget):
|
|
|
94
97
|
def _configuration_toggled(self, is_configured: bool) -> None:
|
|
95
98
|
if is_configured:
|
|
96
99
|
self._command_stream_push_button.setEnabled(True)
|
|
100
|
+
self._command_stream_push_button.setToolTip("Step 3: Start data streaming")
|
|
97
101
|
|
|
98
102
|
self.configure_toggled.emit(is_configured)
|
|
99
103
|
|
|
@@ -156,6 +160,7 @@ class OTBSyncStationWidget(BaseDeviceWidget):
|
|
|
156
160
|
self.ui.commandConnectionPushButton
|
|
157
161
|
)
|
|
158
162
|
self._command_connect_push_button.clicked.connect(self._toggle_connection)
|
|
163
|
+
self._command_connect_push_button.setToolTip("Step 1: Connect to the SyncStation device")
|
|
159
164
|
self._device.connect_toggled.connect(self._connection_toggled)
|
|
160
165
|
|
|
161
166
|
self._command_configure_push_button: QPushButton = (
|
|
@@ -163,11 +168,13 @@ class OTBSyncStationWidget(BaseDeviceWidget):
|
|
|
163
168
|
)
|
|
164
169
|
self._command_configure_push_button.clicked.connect(self._toggle_configuration)
|
|
165
170
|
self._command_configure_push_button.setEnabled(False)
|
|
171
|
+
self._command_configure_push_button.setToolTip("Step 2: Configure device settings (connect first)")
|
|
166
172
|
self._device.configure_toggled.connect(self._configuration_toggled)
|
|
167
173
|
|
|
168
174
|
self._command_stream_push_button: QPushButton = self.ui.commandStreamPushButton
|
|
169
175
|
self._command_stream_push_button.clicked.connect(self._toggle_stream)
|
|
170
176
|
self._command_stream_push_button.setEnabled(False)
|
|
177
|
+
self._command_stream_push_button.setToolTip("Step 3: Start data streaming (configure first)")
|
|
171
178
|
self._device.stream_toggled.connect(self._stream_toggled)
|
|
172
179
|
|
|
173
180
|
# Connection Paramters
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
from functools import partial
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from PySide6.QtGui import QResizeEvent, QWheelEvent
|
|
5
|
+
from PySide6.QtGui import QResizeEvent, QWheelEvent, QFont
|
|
6
6
|
from vispy import app, gloo
|
|
7
7
|
from PySide6.QtWidgets import (
|
|
8
8
|
QVBoxLayout,
|
|
@@ -11,6 +11,8 @@ from PySide6.QtWidgets import (
|
|
|
11
11
|
QCheckBox,
|
|
12
12
|
QGridLayout,
|
|
13
13
|
QSizePolicy,
|
|
14
|
+
QLabel,
|
|
15
|
+
QFrame,
|
|
14
16
|
)
|
|
15
17
|
from PySide6.QtCore import Qt, Signal, QPoint
|
|
16
18
|
import matplotlib.colors as mcolors
|
|
@@ -50,7 +52,45 @@ class BiosignalPlotWidget(QWidget):
|
|
|
50
52
|
self.is_configured: bool = False
|
|
51
53
|
|
|
52
54
|
def _configure_widget(self):
|
|
53
|
-
# Create
|
|
55
|
+
# Create main layout
|
|
56
|
+
self.setLayout(QVBoxLayout())
|
|
57
|
+
self.layout().setContentsMargins(0, 0, 0, 0)
|
|
58
|
+
|
|
59
|
+
# Create placeholder widget (shown when not configured)
|
|
60
|
+
self.placeholder_widget = QFrame(self)
|
|
61
|
+
self.placeholder_widget.setStyleSheet("""
|
|
62
|
+
QFrame {
|
|
63
|
+
background-color: rgba(18, 18, 18, 1);
|
|
64
|
+
border: 2px dashed rgba(100, 100, 100, 0.5);
|
|
65
|
+
border-radius: 8px;
|
|
66
|
+
}
|
|
67
|
+
""")
|
|
68
|
+
placeholder_layout = QVBoxLayout(self.placeholder_widget)
|
|
69
|
+
placeholder_layout.setAlignment(Qt.AlignCenter)
|
|
70
|
+
|
|
71
|
+
# Placeholder icon/text
|
|
72
|
+
self.placeholder_label = QLabel("📊")
|
|
73
|
+
self.placeholder_label.setStyleSheet("font-size: 48px; border: none;")
|
|
74
|
+
self.placeholder_label.setAlignment(Qt.AlignCenter)
|
|
75
|
+
|
|
76
|
+
self.placeholder_text = QLabel("EMG Signal Plot")
|
|
77
|
+
self.placeholder_text.setStyleSheet("color: rgba(150, 150, 150, 1); font-size: 16px; font-weight: bold; border: none;")
|
|
78
|
+
self.placeholder_text.setAlignment(Qt.AlignCenter)
|
|
79
|
+
|
|
80
|
+
self.placeholder_hint = QLabel("Connect and configure a device to view signals")
|
|
81
|
+
self.placeholder_hint.setStyleSheet("color: rgba(100, 100, 100, 1); font-size: 12px; border: none;")
|
|
82
|
+
self.placeholder_hint.setAlignment(Qt.AlignCenter)
|
|
83
|
+
self.placeholder_hint.setWordWrap(True)
|
|
84
|
+
|
|
85
|
+
placeholder_layout.addStretch()
|
|
86
|
+
placeholder_layout.addWidget(self.placeholder_label)
|
|
87
|
+
placeholder_layout.addWidget(self.placeholder_text)
|
|
88
|
+
placeholder_layout.addWidget(self.placeholder_hint)
|
|
89
|
+
placeholder_layout.addStretch()
|
|
90
|
+
|
|
91
|
+
self.layout().addWidget(self.placeholder_widget)
|
|
92
|
+
|
|
93
|
+
# Create scroll_area (hidden initially)
|
|
54
94
|
self.scroll_area = QScrollArea(self)
|
|
55
95
|
self.scroll_area.setHorizontalScrollBarPolicy(
|
|
56
96
|
Qt.ScrollBarAlwaysOff
|
|
@@ -58,9 +98,8 @@ class BiosignalPlotWidget(QWidget):
|
|
|
58
98
|
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
59
99
|
self.scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
60
100
|
self.scroll_area.setLayoutDirection(Qt.RightToLeft)
|
|
101
|
+
self.scroll_area.hide() # Hidden until configured
|
|
61
102
|
|
|
62
|
-
# Create a layout for the VispyFastPlotWidget
|
|
63
|
-
self.setLayout(QVBoxLayout())
|
|
64
103
|
# Add the scroll_area to the layout
|
|
65
104
|
self.layout().addWidget(self.scroll_area)
|
|
66
105
|
|
|
@@ -154,7 +193,7 @@ class BiosignalPlotWidget(QWidget):
|
|
|
154
193
|
self.lines_enabled = np.ones((self.number_of_lines,)).astype(bool)
|
|
155
194
|
|
|
156
195
|
for i in range(self.number_of_lines):
|
|
157
|
-
checkbox = QCheckBox(f"
|
|
196
|
+
checkbox = QCheckBox(f"Ch {i + 1}")
|
|
158
197
|
checkbox.setChecked(True)
|
|
159
198
|
checkbox.stateChanged.connect(partial(self._toggle_line, i))
|
|
160
199
|
checkbox.setStyleSheet("padding-left: 10px;")
|
|
@@ -176,6 +215,10 @@ class BiosignalPlotWidget(QWidget):
|
|
|
176
215
|
else:
|
|
177
216
|
self.container_widget_layout.setRowMinimumHeight(i, 0)
|
|
178
217
|
|
|
218
|
+
# Show plot, hide placeholder
|
|
219
|
+
self.placeholder_widget.hide()
|
|
220
|
+
self.scroll_area.show()
|
|
221
|
+
|
|
179
222
|
self.is_configured = True
|
|
180
223
|
|
|
181
224
|
def update_plot(self, input_data: np.ndarray) -> None:
|
|
@@ -202,6 +245,12 @@ class BiosignalPlotWidget(QWidget):
|
|
|
202
245
|
def reset_data(self) -> None:
|
|
203
246
|
self.canvas.on_reset()
|
|
204
247
|
|
|
248
|
+
def show_placeholder(self) -> None:
|
|
249
|
+
"""Show the placeholder and hide the plot."""
|
|
250
|
+
self.scroll_area.hide()
|
|
251
|
+
self.placeholder_widget.show()
|
|
252
|
+
self.is_configured = False
|
|
253
|
+
|
|
205
254
|
def _toggle_line(self, line_number: int, state: int) -> None:
|
|
206
255
|
is_checked = state == 2
|
|
207
256
|
self.lines_enabled[line_number] = is_checked
|
|
@@ -350,7 +399,6 @@ class VispyFastPlotCanvas(app.Canvas):
|
|
|
350
399
|
plot_data = self.line_data.ravel().astype(np.float32)
|
|
351
400
|
self.program["a_position"].set_data(plot_data)
|
|
352
401
|
self.update()
|
|
353
|
-
self.context.flush()
|
|
354
402
|
|
|
355
403
|
def on_update_color(self, line_number: int, disable: bool = False) -> None:
|
|
356
404
|
# Update alpha value of the line color
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<ui version="4.0">
|
|
3
|
+
<class>DeviceWidgetForm</class>
|
|
4
|
+
<widget class="QWidget" name="DeviceWidgetForm">
|
|
5
|
+
<property name="geometry">
|
|
6
|
+
<rect>
|
|
7
|
+
<x>0</x>
|
|
8
|
+
<y>0</y>
|
|
9
|
+
<width>400</width>
|
|
10
|
+
<height>300</height>
|
|
11
|
+
</rect>
|
|
12
|
+
</property>
|
|
13
|
+
<property name="sizePolicy">
|
|
14
|
+
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
|
15
|
+
<horstretch>0</horstretch>
|
|
16
|
+
<verstretch>1</verstretch>
|
|
17
|
+
</sizepolicy>
|
|
18
|
+
</property>
|
|
19
|
+
<property name="windowTitle">
|
|
20
|
+
<string>Form</string>
|
|
21
|
+
</property>
|
|
22
|
+
<layout class="QGridLayout" name="gridLayout">
|
|
23
|
+
<item row="0" column="0">
|
|
24
|
+
<widget class="QLabel" name="label">
|
|
25
|
+
<property name="text">
|
|
26
|
+
<string>Device</string>
|
|
27
|
+
</property>
|
|
28
|
+
</widget>
|
|
29
|
+
</item>
|
|
30
|
+
<item row="0" column="1">
|
|
31
|
+
<widget class="QComboBox" name="deviceSelectionComboBox"/>
|
|
32
|
+
</item>
|
|
33
|
+
<item row="1" column="0" colspan="2">
|
|
34
|
+
<layout class="QHBoxLayout" name="scrollAreaLayout">
|
|
35
|
+
<property name="spacing">
|
|
36
|
+
<number>6</number>
|
|
37
|
+
</property>
|
|
38
|
+
<property name="leftMargin">
|
|
39
|
+
<number>20</number>
|
|
40
|
+
</property>
|
|
41
|
+
<item>
|
|
42
|
+
<widget class="QScrollArea" name="deviceScrollArea">
|
|
43
|
+
<property name="sizePolicy">
|
|
44
|
+
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
|
45
|
+
<horstretch>0</horstretch>
|
|
46
|
+
<verstretch>1</verstretch>
|
|
47
|
+
</sizepolicy>
|
|
48
|
+
</property>
|
|
49
|
+
<property name="frameShape">
|
|
50
|
+
<enum>QFrame::Shape::NoFrame</enum>
|
|
51
|
+
</property>
|
|
52
|
+
<property name="horizontalScrollBarPolicy">
|
|
53
|
+
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
|
|
54
|
+
</property>
|
|
55
|
+
<property name="verticalScrollBarPolicy">
|
|
56
|
+
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
|
|
57
|
+
</property>
|
|
58
|
+
<property name="widgetResizable">
|
|
59
|
+
<bool>true</bool>
|
|
60
|
+
</property>
|
|
61
|
+
<widget class="QWidget" name="deviceScrollAreaContents">
|
|
62
|
+
<property name="geometry">
|
|
63
|
+
<rect>
|
|
64
|
+
<x>0</x>
|
|
65
|
+
<y>0</y>
|
|
66
|
+
<width>100</width>
|
|
67
|
+
<height>30</height>
|
|
68
|
+
</rect>
|
|
69
|
+
</property>
|
|
70
|
+
<layout class="QHBoxLayout" name="deviceScrollAreaLayout">
|
|
71
|
+
<property name="leftMargin">
|
|
72
|
+
<number>0</number>
|
|
73
|
+
</property>
|
|
74
|
+
<property name="topMargin">
|
|
75
|
+
<number>0</number>
|
|
76
|
+
</property>
|
|
77
|
+
<property name="rightMargin">
|
|
78
|
+
<number>0</number>
|
|
79
|
+
</property>
|
|
80
|
+
<property name="bottomMargin">
|
|
81
|
+
<number>0</number>
|
|
82
|
+
</property>
|
|
83
|
+
<item>
|
|
84
|
+
<spacer name="horizontalSpacerLeft">
|
|
85
|
+
<property name="orientation">
|
|
86
|
+
<enum>Qt::Horizontal</enum>
|
|
87
|
+
</property>
|
|
88
|
+
<property name="sizeHint" stdset="0">
|
|
89
|
+
<size>
|
|
90
|
+
<width>0</width>
|
|
91
|
+
<height>0</height>
|
|
92
|
+
</size>
|
|
93
|
+
</property>
|
|
94
|
+
</spacer>
|
|
95
|
+
</item>
|
|
96
|
+
<item>
|
|
97
|
+
<widget class="QStackedWidget" name="deviceStackedWidget">
|
|
98
|
+
<property name="currentIndex">
|
|
99
|
+
<number>-1</number>
|
|
100
|
+
</property>
|
|
101
|
+
</widget>
|
|
102
|
+
</item>
|
|
103
|
+
<item>
|
|
104
|
+
<spacer name="horizontalSpacerRight">
|
|
105
|
+
<property name="orientation">
|
|
106
|
+
<enum>Qt::Horizontal</enum>
|
|
107
|
+
</property>
|
|
108
|
+
<property name="sizeHint" stdset="0">
|
|
109
|
+
<size>
|
|
110
|
+
<width>0</width>
|
|
111
|
+
<height>0</height>
|
|
112
|
+
</size>
|
|
113
|
+
</property>
|
|
114
|
+
</spacer>
|
|
115
|
+
</item>
|
|
116
|
+
</layout>
|
|
117
|
+
</widget>
|
|
118
|
+
</widget>
|
|
119
|
+
</item>
|
|
120
|
+
<item>
|
|
121
|
+
<widget class="QScrollBar" name="deviceScrollBar">
|
|
122
|
+
<property name="orientation">
|
|
123
|
+
<enum>Qt::Orientation::Vertical</enum>
|
|
124
|
+
</property>
|
|
125
|
+
</widget>
|
|
126
|
+
</item>
|
|
127
|
+
</layout>
|
|
128
|
+
</item>
|
|
129
|
+
</layout>
|
|
130
|
+
</widget>
|
|
131
|
+
<resources/>
|
|
132
|
+
<connections/>
|
|
133
|
+
</ui>
|
|
@@ -6,13 +6,19 @@
|
|
|
6
6
|
<rect>
|
|
7
7
|
<x>0</x>
|
|
8
8
|
<y>0</y>
|
|
9
|
-
<width>
|
|
9
|
+
<width>340</width>
|
|
10
10
|
<height>324</height>
|
|
11
11
|
</rect>
|
|
12
12
|
</property>
|
|
13
13
|
<property name="windowTitle">
|
|
14
14
|
<string>MuoviPlusForm</string>
|
|
15
15
|
</property>
|
|
16
|
+
<property name="maximumSize">
|
|
17
|
+
<size>
|
|
18
|
+
<width>340</width>
|
|
19
|
+
<height>16777215</height>
|
|
20
|
+
</size>
|
|
21
|
+
</property>
|
|
16
22
|
<layout class="QGridLayout" name="gridLayout">
|
|
17
23
|
<item row="4" column="0">
|
|
18
24
|
<spacer name="verticalSpacer">
|
|
@@ -6,13 +6,19 @@
|
|
|
6
6
|
<rect>
|
|
7
7
|
<x>0</x>
|
|
8
8
|
<y>0</y>
|
|
9
|
-
<width>
|
|
9
|
+
<width>340</width>
|
|
10
10
|
<height>324</height>
|
|
11
11
|
</rect>
|
|
12
12
|
</property>
|
|
13
13
|
<property name="windowTitle">
|
|
14
14
|
<string>MuoviForm</string>
|
|
15
15
|
</property>
|
|
16
|
+
<property name="maximumSize">
|
|
17
|
+
<size>
|
|
18
|
+
<width>340</width>
|
|
19
|
+
<height>16777215</height>
|
|
20
|
+
</size>
|
|
21
|
+
</property>
|
|
16
22
|
<layout class="QGridLayout" name="gridLayout">
|
|
17
23
|
<item row="4" column="0">
|
|
18
24
|
<spacer name="verticalSpacer">
|