nucleuskit-toolkit 2.2.0__py3-none-any.whl
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.
- nucleuskit_toolkit/__init__.py +3 -0
- nucleuskit_toolkit/__main__.py +60 -0
- nucleuskit_toolkit/app.py +34 -0
- nucleuskit_toolkit/camera/__init__.py +9 -0
- nucleuskit_toolkit/camera/processing.py +374 -0
- nucleuskit_toolkit/config.py +86 -0
- nucleuskit_toolkit/hermes/__init__.py +0 -0
- nucleuskit_toolkit/hermes/constants.py +77 -0
- nucleuskit_toolkit/hermes/realtime/__init__.py +8 -0
- nucleuskit_toolkit/hermes/realtime/proxy.py +652 -0
- nucleuskit_toolkit/logging_utils.py +68 -0
- nucleuskit_toolkit/mqtt/__init__.py +5 -0
- nucleuskit_toolkit/mqtt/mqtt_controller.py +256 -0
- nucleuskit_toolkit/pipeline.py +194 -0
- nucleuskit_toolkit/position/__init__.py +5 -0
- nucleuskit_toolkit/position/gps_processor.py +91 -0
- nucleuskit_toolkit/session_job.py +28 -0
- nucleuskit_toolkit/shimmer/__init__.py +5 -0
- nucleuskit_toolkit/shimmer/constants.py +32 -0
- nucleuskit_toolkit/shimmer/realtime/__init__.py +5 -0
- nucleuskit_toolkit/shimmer/realtime/proxy.py +279 -0
- nucleuskit_toolkit/tools/__init__.py +1 -0
- nucleuskit_toolkit/tools/eeg_regression.py +91 -0
- nucleuskit_toolkit/tools/recording_merger.py +437 -0
- nucleuskit_toolkit/ui/__init__.py +1 -0
- nucleuskit_toolkit/ui/icons.py +165 -0
- nucleuskit_toolkit/ui/main_window.py +465 -0
- nucleuskit_toolkit/ui/offline_job.py +144 -0
- nucleuskit_toolkit/ui/pages/__init__.py +1 -0
- nucleuskit_toolkit/ui/pages/about_page.py +88 -0
- nucleuskit_toolkit/ui/pages/channel_fixer_page.py +205 -0
- nucleuskit_toolkit/ui/pages/channel_gain_page.py +362 -0
- nucleuskit_toolkit/ui/pages/eeg_regression_page.py +385 -0
- nucleuskit_toolkit/ui/pages/mqtt_controller_page.py +394 -0
- nucleuskit_toolkit/ui/pages/offline_page.py +754 -0
- nucleuskit_toolkit/ui/pages/playback_page.py +1524 -0
- nucleuskit_toolkit/ui/pages/ppg_fixer_page.py +199 -0
- nucleuskit_toolkit/ui/pages/realtime_viewer_page.py +954 -0
- nucleuskit_toolkit/ui/pages/recording_merge_page.py +395 -0
- nucleuskit_toolkit/ui/pages/revert_original_page.py +128 -0
- nucleuskit_toolkit/ui/pages/settings_page.py +69 -0
- nucleuskit_toolkit/ui/pages/tools_menu_page.py +42 -0
- nucleuskit_toolkit/ui/plot_style.py +155 -0
- nucleuskit_toolkit/ui/realtime_eeg_plot.py +244 -0
- nucleuskit_toolkit/ui/realtime_hermes_motion_plot.py +162 -0
- nucleuskit_toolkit/ui/realtime_nlms_config_dialog.py +101 -0
- nucleuskit_toolkit/ui/realtime_shimmer_plot.py +135 -0
- nucleuskit_toolkit/ui/resources/__init__.py +1 -0
- nucleuskit_toolkit/ui/resources/branding/__init__.py +1 -0
- nucleuskit_toolkit/ui/resources/branding/logo.png +0 -0
- nucleuskit_toolkit/ui/session_inspect.py +226 -0
- nucleuskit_toolkit/ui/theme.py +330 -0
- nucleuskit_toolkit/ui/widgets.py +190 -0
- nucleuskit_toolkit-2.2.0.dist-info/METADATA +351 -0
- nucleuskit_toolkit-2.2.0.dist-info/RECORD +59 -0
- nucleuskit_toolkit-2.2.0.dist-info/WHEEL +5 -0
- nucleuskit_toolkit-2.2.0.dist-info/entry_points.txt +2 -0
- nucleuskit_toolkit-2.2.0.dist-info/licenses/LICENSE +675 -0
- nucleuskit_toolkit-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""MQTT Controller page — connect to a Vizia Mobile broker and send recording commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from PySide6.QtCore import QObject, QSettings, Qt, Signal
|
|
6
|
+
from PySide6.QtGui import QFont
|
|
7
|
+
from PySide6.QtWidgets import (
|
|
8
|
+
QComboBox,
|
|
9
|
+
QFormLayout,
|
|
10
|
+
QGroupBox,
|
|
11
|
+
QHBoxLayout,
|
|
12
|
+
QLabel,
|
|
13
|
+
QLineEdit,
|
|
14
|
+
QMessageBox,
|
|
15
|
+
QPlainTextEdit,
|
|
16
|
+
QPushButton,
|
|
17
|
+
QSizePolicy,
|
|
18
|
+
QSpinBox,
|
|
19
|
+
QSplitter,
|
|
20
|
+
QVBoxLayout,
|
|
21
|
+
QWidget,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from nucleuskit_toolkit.mqtt.mqtt_controller import (
|
|
25
|
+
DEFAULT_BROKER_PORT,
|
|
26
|
+
DEFAULT_COMMAND_TOPIC_SUFFIX,
|
|
27
|
+
DEFAULT_STATUS_TOPIC,
|
|
28
|
+
TARGET_ALL,
|
|
29
|
+
DeviceStatus,
|
|
30
|
+
MqttController,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_SETTINGS_ORG = "NucleusKit"
|
|
34
|
+
_SETTINGS_APP = "MqttController"
|
|
35
|
+
_KEY_HOST = "broker/host"
|
|
36
|
+
_KEY_PORT = "broker/port"
|
|
37
|
+
_KEY_STATUS_TOPIC = "broker/status_topic"
|
|
38
|
+
_KEY_CMD_SUFFIX = "broker/command_suffix"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _MqttBridge(QObject):
|
|
42
|
+
"""
|
|
43
|
+
Bridges paho callbacks (arbitrary thread) to Qt signals (GUI thread).
|
|
44
|
+
Must live on the GUI thread so signal delivery is automatic via the event loop.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
log_line = Signal(str)
|
|
48
|
+
device_updated = Signal(object) # DeviceStatus
|
|
49
|
+
connected = Signal()
|
|
50
|
+
disconnected = Signal()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MqttControllerPage(QWidget):
|
|
54
|
+
"""Full-page MQTT controller: broker configuration, device discovery, commands."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def __init__(self, parent: QWidget | None = None) -> None:
|
|
58
|
+
super().__init__(parent)
|
|
59
|
+
|
|
60
|
+
self._bridge = _MqttBridge(self)
|
|
61
|
+
self._bridge.log_line.connect(self._append_log)
|
|
62
|
+
self._bridge.device_updated.connect(self._on_device_updated)
|
|
63
|
+
self._bridge.connected.connect(self._on_connected)
|
|
64
|
+
self._bridge.disconnected.connect(self._on_disconnected)
|
|
65
|
+
|
|
66
|
+
self._controller: MqttController | None = None
|
|
67
|
+
|
|
68
|
+
self._build_ui()
|
|
69
|
+
self._load_settings()
|
|
70
|
+
|
|
71
|
+
# ── UI construction ──────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
def _build_ui(self) -> None:
|
|
74
|
+
root = QVBoxLayout(self)
|
|
75
|
+
root.setContentsMargins(0, 0, 0, 0)
|
|
76
|
+
|
|
77
|
+
# ── Main splitter: left panel / right log ────────────────────────
|
|
78
|
+
splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
79
|
+
splitter.setChildrenCollapsible(False)
|
|
80
|
+
|
|
81
|
+
left = QWidget()
|
|
82
|
+
left_col = QVBoxLayout(left)
|
|
83
|
+
left_col.setContentsMargins(0, 0, 8, 0)
|
|
84
|
+
|
|
85
|
+
left_col.addWidget(self._build_broker_group())
|
|
86
|
+
left_col.addSpacing(8)
|
|
87
|
+
left_col.addWidget(self._build_devices_group())
|
|
88
|
+
left_col.addSpacing(8)
|
|
89
|
+
left_col.addWidget(self._build_commands_group())
|
|
90
|
+
left_col.addStretch(1)
|
|
91
|
+
|
|
92
|
+
right = QWidget()
|
|
93
|
+
right_col = QVBoxLayout(right)
|
|
94
|
+
right_col.setContentsMargins(8, 0, 0, 0)
|
|
95
|
+
right_col.addWidget(self._build_log_group())
|
|
96
|
+
|
|
97
|
+
splitter.addWidget(left)
|
|
98
|
+
splitter.addWidget(right)
|
|
99
|
+
splitter.setStretchFactor(0, 1)
|
|
100
|
+
splitter.setStretchFactor(1, 2)
|
|
101
|
+
|
|
102
|
+
root.addWidget(splitter, stretch=1)
|
|
103
|
+
|
|
104
|
+
def _build_broker_group(self) -> QGroupBox:
|
|
105
|
+
group = QGroupBox("Broker connection")
|
|
106
|
+
form = QFormLayout(group)
|
|
107
|
+
|
|
108
|
+
self._host_edit = QLineEdit()
|
|
109
|
+
self._host_edit.setPlaceholderText("e.g. 192.168.1.100")
|
|
110
|
+
self._host_edit.setToolTip("IP address or hostname of the MQTT broker.")
|
|
111
|
+
form.addRow("Host:", self._host_edit)
|
|
112
|
+
|
|
113
|
+
self._port_spin = QSpinBox()
|
|
114
|
+
self._port_spin.setRange(1, 65535)
|
|
115
|
+
self._port_spin.setValue(DEFAULT_BROKER_PORT)
|
|
116
|
+
form.addRow("Port:", self._port_spin)
|
|
117
|
+
|
|
118
|
+
self._status_topic_edit = QLineEdit()
|
|
119
|
+
self._status_topic_edit.setPlaceholderText(DEFAULT_STATUS_TOPIC)
|
|
120
|
+
self._status_topic_edit.setToolTip(
|
|
121
|
+
"Topic on which Vizia devices publish status packets.\n"
|
|
122
|
+
"Default: vizia/status"
|
|
123
|
+
)
|
|
124
|
+
form.addRow("Status topic:", self._status_topic_edit)
|
|
125
|
+
|
|
126
|
+
self._cmd_suffix_edit = QLineEdit()
|
|
127
|
+
self._cmd_suffix_edit.setPlaceholderText(DEFAULT_COMMAND_TOPIC_SUFFIX)
|
|
128
|
+
self._cmd_suffix_edit.setToolTip(
|
|
129
|
+
"Suffix appended after {recorderId}/ when publishing commands.\n"
|
|
130
|
+
"Default: control/"
|
|
131
|
+
)
|
|
132
|
+
form.addRow("Command suffix:", self._cmd_suffix_edit)
|
|
133
|
+
|
|
134
|
+
btn_row = QHBoxLayout()
|
|
135
|
+
self._connect_btn = QPushButton("Connect")
|
|
136
|
+
self._connect_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
137
|
+
self._connect_btn.clicked.connect(self._on_connect_clicked)
|
|
138
|
+
self._disconnect_btn = QPushButton("Disconnect")
|
|
139
|
+
self._disconnect_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
140
|
+
self._disconnect_btn.setEnabled(False)
|
|
141
|
+
self._disconnect_btn.clicked.connect(self._on_disconnect_clicked)
|
|
142
|
+
btn_row.addWidget(self._connect_btn)
|
|
143
|
+
btn_row.addWidget(self._disconnect_btn)
|
|
144
|
+
|
|
145
|
+
self._status_label = QLabel("Not connected")
|
|
146
|
+
self._status_label.setProperty("role", "hint")
|
|
147
|
+
|
|
148
|
+
form.addRow(btn_row)
|
|
149
|
+
form.addRow("Status:", self._status_label)
|
|
150
|
+
|
|
151
|
+
return group
|
|
152
|
+
|
|
153
|
+
def _build_devices_group(self) -> QGroupBox:
|
|
154
|
+
group = QGroupBox("Discovered devices")
|
|
155
|
+
col = QVBoxLayout(group)
|
|
156
|
+
|
|
157
|
+
hint = QLabel(
|
|
158
|
+
"Devices are discovered automatically from incoming status packets.\n"
|
|
159
|
+
"Select a target below before sending commands."
|
|
160
|
+
)
|
|
161
|
+
hint.setWordWrap(True)
|
|
162
|
+
hint.setProperty("role", "hint")
|
|
163
|
+
col.addWidget(hint)
|
|
164
|
+
|
|
165
|
+
self._device_combo = QComboBox()
|
|
166
|
+
self._device_combo.addItem("All devices", TARGET_ALL)
|
|
167
|
+
self._device_combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
168
|
+
col.addWidget(self._device_combo)
|
|
169
|
+
|
|
170
|
+
self._device_status_label = QLabel("")
|
|
171
|
+
self._device_status_label.setProperty("role", "hint")
|
|
172
|
+
self._device_status_label.setWordWrap(True)
|
|
173
|
+
col.addWidget(self._device_status_label)
|
|
174
|
+
|
|
175
|
+
return group
|
|
176
|
+
|
|
177
|
+
def _build_commands_group(self) -> QGroupBox:
|
|
178
|
+
group = QGroupBox("Commands")
|
|
179
|
+
col = QVBoxLayout(group)
|
|
180
|
+
|
|
181
|
+
rec_row = QHBoxLayout()
|
|
182
|
+
self._start_btn = QPushButton("Start recording")
|
|
183
|
+
self._start_btn.setEnabled(False)
|
|
184
|
+
self._start_btn.clicked.connect(self._on_start_recording)
|
|
185
|
+
self._stop_btn = QPushButton("Stop recording")
|
|
186
|
+
self._stop_btn.setEnabled(False)
|
|
187
|
+
self._stop_btn.clicked.connect(self._on_stop_recording)
|
|
188
|
+
rec_row.addWidget(self._start_btn)
|
|
189
|
+
rec_row.addWidget(self._stop_btn)
|
|
190
|
+
col.addLayout(rec_row)
|
|
191
|
+
|
|
192
|
+
col.addSpacing(4)
|
|
193
|
+
|
|
194
|
+
tag_form = QFormLayout()
|
|
195
|
+
self._tag_code_edit = QLineEdit()
|
|
196
|
+
self._tag_code_edit.setPlaceholderText("e.g. STIMULUS_ON")
|
|
197
|
+
tag_form.addRow("Tag code:", self._tag_code_edit)
|
|
198
|
+
|
|
199
|
+
self._tag_values_edit = QLineEdit()
|
|
200
|
+
self._tag_values_edit.setPlaceholderText("optional detail string")
|
|
201
|
+
tag_form.addRow("Tag values:", self._tag_values_edit)
|
|
202
|
+
col.addLayout(tag_form)
|
|
203
|
+
|
|
204
|
+
self._tag_btn = QPushButton("Send tag")
|
|
205
|
+
self._tag_btn.setEnabled(False)
|
|
206
|
+
self._tag_btn.clicked.connect(self._on_send_tag)
|
|
207
|
+
col.addWidget(self._tag_btn)
|
|
208
|
+
|
|
209
|
+
return group
|
|
210
|
+
|
|
211
|
+
def _build_log_group(self) -> QGroupBox:
|
|
212
|
+
group = QGroupBox("Connection log")
|
|
213
|
+
col = QVBoxLayout(group)
|
|
214
|
+
|
|
215
|
+
self._log_view = QPlainTextEdit()
|
|
216
|
+
self._log_view.setReadOnly(True)
|
|
217
|
+
self._log_view.setMaximumBlockCount(500)
|
|
218
|
+
font = QFont("Monospace")
|
|
219
|
+
font.setStyleHint(QFont.StyleHint.TypeWriter)
|
|
220
|
+
font.setPointSize(9)
|
|
221
|
+
self._log_view.setFont(font)
|
|
222
|
+
col.addWidget(self._log_view)
|
|
223
|
+
|
|
224
|
+
clear_btn = QPushButton("Clear log")
|
|
225
|
+
clear_btn.setProperty("secondary", True)
|
|
226
|
+
clear_btn.clicked.connect(self._log_view.clear)
|
|
227
|
+
col.addWidget(clear_btn)
|
|
228
|
+
|
|
229
|
+
return group
|
|
230
|
+
|
|
231
|
+
# ── Settings persistence ─────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
def _load_settings(self) -> None:
|
|
234
|
+
s = QSettings(_SETTINGS_ORG, _SETTINGS_APP)
|
|
235
|
+
self._host_edit.setText(s.value(_KEY_HOST, "", type=str))
|
|
236
|
+
self._port_spin.setValue(int(s.value(_KEY_PORT, DEFAULT_BROKER_PORT)))
|
|
237
|
+
self._status_topic_edit.setText(s.value(_KEY_STATUS_TOPIC, "", type=str))
|
|
238
|
+
self._cmd_suffix_edit.setText(s.value(_KEY_CMD_SUFFIX, "", type=str))
|
|
239
|
+
|
|
240
|
+
def _save_settings(self) -> None:
|
|
241
|
+
s = QSettings(_SETTINGS_ORG, _SETTINGS_APP)
|
|
242
|
+
s.setValue(_KEY_HOST, self._host_edit.text().strip())
|
|
243
|
+
s.setValue(_KEY_PORT, self._port_spin.value())
|
|
244
|
+
s.setValue(_KEY_STATUS_TOPIC, self._status_topic_edit.text().strip())
|
|
245
|
+
s.setValue(_KEY_CMD_SUFFIX, self._cmd_suffix_edit.text().strip())
|
|
246
|
+
|
|
247
|
+
# ── Connection actions ───────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
def _on_connect_clicked(self) -> None:
|
|
250
|
+
host = self._host_edit.text().strip()
|
|
251
|
+
if not host:
|
|
252
|
+
QMessageBox.warning(self, "Host required", "Enter the broker IP address or hostname.")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
port = self._port_spin.value()
|
|
256
|
+
status_topic = self._status_topic_edit.text().strip() or DEFAULT_STATUS_TOPIC
|
|
257
|
+
cmd_suffix = self._cmd_suffix_edit.text().strip() or DEFAULT_COMMAND_TOPIC_SUFFIX
|
|
258
|
+
|
|
259
|
+
self._save_settings()
|
|
260
|
+
|
|
261
|
+
if self._controller is not None:
|
|
262
|
+
self._controller.disconnect()
|
|
263
|
+
self._controller = None
|
|
264
|
+
|
|
265
|
+
self._device_combo.clear()
|
|
266
|
+
self._device_combo.addItem("All devices", TARGET_ALL)
|
|
267
|
+
self._device_status_label.setText("")
|
|
268
|
+
|
|
269
|
+
self._controller = MqttController(
|
|
270
|
+
broker_host=host,
|
|
271
|
+
broker_port=port,
|
|
272
|
+
status_topic=status_topic,
|
|
273
|
+
command_topic_suffix=cmd_suffix,
|
|
274
|
+
on_log=self._bridge.log_line.emit,
|
|
275
|
+
on_device_update=self._bridge.device_updated.emit,
|
|
276
|
+
on_connected=self._bridge.connected.emit,
|
|
277
|
+
on_disconnected=self._bridge.disconnected.emit,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
self._connect_btn.setEnabled(False)
|
|
281
|
+
self._status_label.setText("Connecting …")
|
|
282
|
+
self._controller.connect()
|
|
283
|
+
|
|
284
|
+
def _on_disconnect_clicked(self) -> None:
|
|
285
|
+
if self._controller is not None:
|
|
286
|
+
self._controller.disconnect()
|
|
287
|
+
self._controller = None
|
|
288
|
+
self._set_disconnected_state()
|
|
289
|
+
|
|
290
|
+
# ── Bridge callbacks (GUI thread) ────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
def _on_connected(self) -> None:
|
|
293
|
+
self._disconnect_btn.setEnabled(True)
|
|
294
|
+
self._connect_btn.setEnabled(False)
|
|
295
|
+
self._start_btn.setEnabled(True)
|
|
296
|
+
self._stop_btn.setEnabled(True)
|
|
297
|
+
self._tag_btn.setEnabled(True)
|
|
298
|
+
self._status_label.setText("Connected")
|
|
299
|
+
|
|
300
|
+
def _on_disconnected(self) -> None:
|
|
301
|
+
self._set_disconnected_state()
|
|
302
|
+
|
|
303
|
+
def _set_disconnected_state(self) -> None:
|
|
304
|
+
self._connect_btn.setEnabled(True)
|
|
305
|
+
self._disconnect_btn.setEnabled(False)
|
|
306
|
+
self._start_btn.setEnabled(False)
|
|
307
|
+
self._stop_btn.setEnabled(False)
|
|
308
|
+
self._tag_btn.setEnabled(False)
|
|
309
|
+
self._status_label.setText("Not connected")
|
|
310
|
+
|
|
311
|
+
def _on_device_updated(self, status: DeviceStatus) -> None:
|
|
312
|
+
device_id = status.device_id
|
|
313
|
+
|
|
314
|
+
found = False
|
|
315
|
+
for i in range(self._device_combo.count()):
|
|
316
|
+
if self._device_combo.itemData(i) == device_id:
|
|
317
|
+
self._device_combo.setItemText(
|
|
318
|
+
i,
|
|
319
|
+
f"{device_id} (recording={'yes' if status.recording else 'no'}, battery={status.battery}%)",
|
|
320
|
+
)
|
|
321
|
+
found = True
|
|
322
|
+
break
|
|
323
|
+
|
|
324
|
+
if not found:
|
|
325
|
+
self._device_combo.addItem(
|
|
326
|
+
f"{device_id} (recording={'yes' if status.recording else 'no'}, battery={status.battery}%)",
|
|
327
|
+
device_id,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
self._refresh_device_status_label()
|
|
331
|
+
|
|
332
|
+
def _refresh_device_status_label(self) -> None:
|
|
333
|
+
if self._controller is None:
|
|
334
|
+
self._device_status_label.setText("")
|
|
335
|
+
return
|
|
336
|
+
devices = self._controller.discovered_devices
|
|
337
|
+
if not devices:
|
|
338
|
+
self._device_status_label.setText("No devices seen yet.")
|
|
339
|
+
else:
|
|
340
|
+
lines = [
|
|
341
|
+
f"• {d.device_id}: {'● recording' if d.recording else '○ idle'}, battery {d.battery}%"
|
|
342
|
+
for d in devices
|
|
343
|
+
]
|
|
344
|
+
self._device_status_label.setText("\n".join(lines))
|
|
345
|
+
|
|
346
|
+
# ── Command actions ──────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
def _selected_device_id(self) -> str:
|
|
349
|
+
data = self._device_combo.currentData()
|
|
350
|
+
return data if data is not None else TARGET_ALL
|
|
351
|
+
|
|
352
|
+
def _on_start_recording(self) -> None:
|
|
353
|
+
if self._controller is None:
|
|
354
|
+
return
|
|
355
|
+
self._controller.send_start_recording(self._selected_device_id())
|
|
356
|
+
|
|
357
|
+
def _on_stop_recording(self) -> None:
|
|
358
|
+
if self._controller is None:
|
|
359
|
+
return
|
|
360
|
+
self._controller.send_stop_recording(self._selected_device_id())
|
|
361
|
+
|
|
362
|
+
def _on_send_tag(self) -> None:
|
|
363
|
+
if self._controller is None:
|
|
364
|
+
return
|
|
365
|
+
code = self._tag_code_edit.text().strip()
|
|
366
|
+
if not code:
|
|
367
|
+
QMessageBox.warning(self, "Tag code required", "Enter a tag code before sending.")
|
|
368
|
+
return
|
|
369
|
+
values = self._tag_values_edit.text().strip()
|
|
370
|
+
self._controller.send_tag(code, values, self._selected_device_id())
|
|
371
|
+
|
|
372
|
+
# ── Log ──────────────────────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
def _append_log(self, msg: str) -> None:
|
|
375
|
+
self._log_view.appendPlainText(msg)
|
|
376
|
+
self._log_view.verticalScrollBar().setValue(
|
|
377
|
+
self._log_view.verticalScrollBar().maximum()
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# ── Navigation ───────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
def can_leave(self) -> bool:
|
|
383
|
+
"""Called by the main window before another page is shown."""
|
|
384
|
+
if self._controller is not None and self._controller.is_connected:
|
|
385
|
+
answer = QMessageBox.question(
|
|
386
|
+
self,
|
|
387
|
+
"Disconnect?",
|
|
388
|
+
"You are connected to a broker. Disconnect and leave this page?",
|
|
389
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
|
|
390
|
+
)
|
|
391
|
+
if answer != QMessageBox.StandardButton.Yes:
|
|
392
|
+
return False
|
|
393
|
+
self._on_disconnect_clicked()
|
|
394
|
+
return True
|