cfclient 2017.4__py3-none-any.whl → 2025.12.1__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.
- cfclient/__init__.py +16 -11
- cfclient/configs/config.json +4 -3
- cfclient/configs/input/Generic_OS_X.json +1 -0
- cfclient/configs/input/Joystick.json +1 -0
- cfclient/configs/input/PS3_Mode_1.json +1 -0
- cfclient/configs/input/PS3_Mode_2.json +1 -0
- cfclient/configs/input/PS3_Mode_3.json +1 -0
- cfclient/configs/input/PS4_Mode_1.json +1 -0
- cfclient/configs/input/PS4_Mode_2.json +1 -0
- cfclient/configs/input/PS4_shoulder_btns_yaw.json +1 -0
- cfclient/configs/input/xbox360_mode1.json +1 -0
- cfclient/configs/log/PID_tuning/Attitude.json +46 -0
- cfclient/configs/log/PID_tuning/Attitude_rate.json +46 -0
- cfclient/configs/log/PID_tuning/Position.json +46 -0
- cfclient/configs/log/PID_tuning/Velocity.json +46 -0
- cfclient/configs/log/PID_tuning_components/Pitch.json +22 -0
- cfclient/configs/log/PID_tuning_components/Pitch_rate.json +22 -0
- cfclient/configs/log/PID_tuning_components/Position_x.json +22 -0
- cfclient/configs/log/PID_tuning_components/Position_y.json +22 -0
- cfclient/configs/log/PID_tuning_components/Position_z.json +22 -0
- cfclient/configs/log/PID_tuning_components/Roll.json +22 -0
- cfclient/configs/log/PID_tuning_components/Roll_rate.json +22 -0
- cfclient/configs/log/PID_tuning_components/Velocity_x.json +22 -0
- cfclient/configs/log/PID_tuning_components/Velocity_y.json +22 -0
- cfclient/configs/log/PID_tuning_components/Velocity_z.json +22 -0
- cfclient/configs/log/PID_tuning_components/Yaw.json +22 -0
- cfclient/configs/log/PID_tuning_components/Yaw_rate.json +22 -0
- cfclient/gui.py +44 -9
- cfclient/headless.py +3 -12
- cfclient/resources/log_param_doc.json +1 -0
- cfclient/ui/connectivity_manager.py +198 -0
- cfclient/ui/dialogs/about.py +53 -36
- cfclient/ui/dialogs/about.ui +23 -3
- cfclient/ui/dialogs/anchor_position_dialog.py +252 -0
- cfclient/ui/dialogs/anchor_position_dialog.ui +138 -0
- cfclient/ui/dialogs/basestation_mode_dialog.py +185 -0
- cfclient/ui/dialogs/basestation_mode_dialog.ui +186 -0
- cfclient/ui/dialogs/bootloader.py +448 -85
- cfclient/ui/dialogs/bootloader.ui +387 -134
- cfclient/ui/dialogs/cf2config.py +4 -4
- cfclient/ui/dialogs/cf2config.ui +3 -4
- cfclient/ui/dialogs/inputconfigdialogue.py +24 -19
- cfclient/ui/dialogs/inputconfigdialogue.ui +53 -30
- cfclient/ui/dialogs/lighthouse_bs_geometry_dialog.py +220 -0
- cfclient/ui/dialogs/lighthouse_bs_geometry_dialog.ui +110 -0
- cfclient/ui/dialogs/lighthouse_system_type_dialog.py +93 -0
- cfclient/ui/dialogs/lighthouse_system_type_dialog.ui +121 -0
- cfclient/ui/dialogs/logconfigdialogue.py +401 -101
- cfclient/ui/dialogs/logconfigdialogue.ui +117 -72
- cfclient/ui/icons/bl.webp +0 -0
- cfclient/ui/icons/bolt.webp +0 -0
- cfclient/ui/icons/cf21.webp +0 -0
- cfclient/ui/icons/checkmark_black.png +0 -0
- cfclient/ui/icons/checkmark_white.png +0 -0
- cfclient/ui/icons/create.png +0 -0
- cfclient/ui/icons/delete.png +0 -0
- cfclient/ui/icons/flapper.webp +0 -0
- cfclient/ui/icons/tag.webp +0 -0
- cfclient/ui/main.py +328 -258
- cfclient/ui/main.ui +184 -80
- cfclient/ui/pluginhelper.py +7 -1
- cfclient/ui/pose_logger.py +116 -0
- cfclient/ui/tab_toolbox.py +208 -0
- cfclient/ui/tabs/ColorLEDTab.py +752 -0
- cfclient/ui/tabs/ConsoleTab.py +48 -13
- cfclient/ui/{toolboxes → tabs}/CrtpSharkToolbox.py +19 -34
- cfclient/ui/tabs/ExampleTab.py +9 -16
- cfclient/ui/tabs/FlightTab.py +437 -325
- cfclient/ui/tabs/GpsTab.py +14 -20
- cfclient/ui/tabs/LEDRingTab.py +277 -0
- cfclient/ui/tabs/LogBlockDebugTab.py +20 -27
- cfclient/ui/tabs/LogBlockTab.py +35 -35
- cfclient/ui/tabs/LogClientTab.py +85 -0
- cfclient/ui/tabs/LogTab.py +50 -27
- cfclient/ui/tabs/ParamTab.py +443 -57
- cfclient/ui/tabs/PlotTab.py +23 -25
- cfclient/ui/tabs/TuningTab.py +292 -0
- cfclient/ui/tabs/__init__.py +12 -2
- cfclient/ui/tabs/colorLEDTab.ui +624 -0
- cfclient/ui/tabs/consoleTab.ui +46 -0
- cfclient/ui/tabs/flightActionContainer.ui +103 -0
- cfclient/ui/tabs/flightTab.ui +724 -237
- cfclient/ui/tabs/{ledTab.ui → ledRingTab.ui} +63 -46
- cfclient/ui/tabs/lighthouse_tab.py +714 -0
- cfclient/ui/tabs/lighthouse_tab.ui +430 -0
- cfclient/ui/tabs/locopositioning_tab.py +606 -389
- cfclient/ui/tabs/locopositioning_tab.ui +370 -253
- cfclient/ui/tabs/logClientTab.ui +52 -0
- cfclient/ui/tabs/logTab.ui +1 -1
- cfclient/ui/tabs/paramTab.ui +204 -3
- cfclient/ui/tabs/tuningTab.ui +773 -0
- cfclient/ui/widgets/ai.py +37 -39
- cfclient/ui/widgets/hexspinbox.py +16 -10
- cfclient/ui/widgets/plotter.ui +39 -47
- cfclient/ui/widgets/plotwidget.py +57 -22
- cfclient/ui/widgets/super_slider.py +112 -0
- cfclient/ui/wizards/__init__.py +0 -0
- cfclient/ui/wizards/bslh_1.png +0 -0
- cfclient/ui/wizards/bslh_2.png +0 -0
- cfclient/ui/wizards/bslh_3.png +0 -0
- cfclient/ui/wizards/bslh_4.png +0 -0
- cfclient/ui/wizards/bslh_5.png +0 -0
- cfclient/ui/wizards/lighthouse_geo_bs_estimation_wizard.py +465 -0
- cfclient/utils/config_manager.py +5 -4
- cfclient/utils/input/__init__.py +77 -19
- cfclient/utils/input/inputinterfaces/wiimote.py +2 -2
- cfclient/utils/input/inputreaderinterface.py +17 -7
- cfclient/utils/input/inputreaders/__init__.py +17 -0
- cfclient/utils/logconfigreader.py +245 -25
- cfclient/utils/logdatawriter.py +3 -1
- cfclient/utils/periodictimer.py +1 -1
- cfclient/utils/ui.py +336 -0
- cfclient/utils/zmq_led_driver.py +5 -0
- cfclient/utils/zmq_param.py +6 -0
- cfclient/version.py +34 -1
- cfclient-2025.12.1.dist-info/METADATA +70 -0
- cfclient-2025.12.1.dist-info/RECORD +152 -0
- {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/WHEEL +1 -1
- {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/entry_points.txt +0 -1
- cfclient-2025.12.1.dist-info/licenses/LICENSE.txt +350 -0
- {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/top_level.txt +1 -0
- cfconfig/Makefile +51 -0
- cfconfig/configblock.py +111 -0
- cfloader/__init__.py +41 -55
- cfzmq/__init__.py +22 -14
- cfclient/ui/dialogs/cf1config.py +0 -265
- cfclient/ui/dialogs/cf1config.ui +0 -260
- cfclient/ui/tab.py +0 -96
- cfclient/ui/tabs/LEDTab.py +0 -169
- cfclient/ui/toolboxes/ConsoleToolbox.py +0 -69
- cfclient/ui/toolboxes/DebugDriverToolbox.py +0 -107
- cfclient/ui/toolboxes/__init__.py +0 -45
- cfclient/ui/toolboxes/consoleToolbox.ui +0 -62
- cfclient/ui/toolboxes/debugDriverToolbox.ui +0 -86
- cfclient-2017.4.dist-info/DESCRIPTION.rst +0 -3
- cfclient-2017.4.dist-info/METADATA +0 -22
- cfclient-2017.4.dist-info/RECORD +0 -104
- cfclient-2017.4.dist-info/metadata.json +0 -1
- /cfclient/{icon-256.png → ui/icons/icon-256.png} +0 -0
- /cfclient/ui/{toolboxes → tabs}/crtpSharkToolbox.ui +0 -0
cfclient/ui/tabs/ParamTab.py
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
|
|
8
8
|
# || || /_____/_/\__/\___/_/ \__,_/ /___/\___/
|
|
9
9
|
#
|
|
10
|
-
# Copyright (C) 2011-
|
|
10
|
+
# Copyright (C) 2011-2023 Bitcraze AB
|
|
11
11
|
#
|
|
12
12
|
# Crazyflie Nano Quadcopter Client
|
|
13
13
|
#
|
|
@@ -31,50 +31,60 @@ to edit them.
|
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
33
|
import logging
|
|
34
|
+
from threading import Event
|
|
34
35
|
|
|
35
|
-
from
|
|
36
|
-
from
|
|
37
|
-
from
|
|
38
|
-
from
|
|
36
|
+
from PyQt6 import uic, QtCore
|
|
37
|
+
from PyQt6.QtCore import QSortFilterProxyModel, Qt, pyqtSignal
|
|
38
|
+
from PyQt6.QtCore import QAbstractItemModel, QModelIndex, QVariant
|
|
39
|
+
from PyQt6.QtGui import QBrush, QColor
|
|
40
|
+
from PyQt6.QtWidgets import QHeaderView, QFileDialog, QMessageBox
|
|
41
|
+
|
|
42
|
+
from cflib.crazyflie.param import PersistentParamState
|
|
43
|
+
from cflib.localization import ParamFileManager
|
|
39
44
|
|
|
40
45
|
import cfclient
|
|
41
|
-
from cfclient.ui.
|
|
46
|
+
from cfclient.ui.tab_toolbox import TabToolbox
|
|
47
|
+
from cfclient.utils.logconfigreader import FILE_REGEX_YAML
|
|
42
48
|
|
|
43
49
|
__author__ = 'Bitcraze AB'
|
|
44
50
|
__all__ = ['ParamTab']
|
|
45
51
|
|
|
46
|
-
param_tab_class = uic.loadUiType(
|
|
47
|
-
cfclient.module_path + "/ui/tabs/paramTab.ui")[0]
|
|
52
|
+
param_tab_class = uic.loadUiType(cfclient.module_path + "/ui/tabs/paramTab.ui")[0]
|
|
48
53
|
|
|
49
54
|
logger = logging.getLogger(__name__)
|
|
50
55
|
|
|
51
56
|
|
|
57
|
+
def round_if_float(value):
|
|
58
|
+
"""If the value is float, we limit to 5 significat numbers"""
|
|
59
|
+
try:
|
|
60
|
+
value = float(value)
|
|
61
|
+
value = f'{value:.5g}'
|
|
62
|
+
except ValueError:
|
|
63
|
+
pass
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
52
67
|
class ParamChildItem(object):
|
|
53
68
|
"""Represents a leaf-node in the tree-view (one parameter)"""
|
|
54
69
|
|
|
55
|
-
def __init__(self, parent, name, crazyflie):
|
|
70
|
+
def __init__(self, parent, name, persistent, crazyflie):
|
|
56
71
|
"""Initialize the node"""
|
|
57
72
|
self.parent = parent
|
|
58
73
|
self.name = name
|
|
59
74
|
self.ctype = None
|
|
60
75
|
self.access = None
|
|
76
|
+
self.persistent = False
|
|
61
77
|
self.value = ""
|
|
62
78
|
self._cf = crazyflie
|
|
63
79
|
self.is_updating = True
|
|
80
|
+
self.state = None
|
|
81
|
+
self.stored_value = ""
|
|
64
82
|
|
|
65
83
|
def updated(self, name, value):
|
|
66
84
|
"""Callback from the param layer when a parameter has been updated"""
|
|
67
|
-
self.value = value
|
|
85
|
+
self.value = round_if_float(value)
|
|
68
86
|
self.is_updating = False
|
|
69
|
-
self.parent.model.
|
|
70
|
-
|
|
71
|
-
def set_value(self, value):
|
|
72
|
-
"""Send the update value to the Crazyflie. It will automatically be
|
|
73
|
-
read again after sending and then the updated callback will be
|
|
74
|
-
called"""
|
|
75
|
-
complete_name = "%s.%s" % (self.parent.name, self.name)
|
|
76
|
-
self._cf.param.set_value(complete_name, value)
|
|
77
|
-
self.is_updating = True
|
|
87
|
+
self.parent.model.proxy.dataChanged.emit(QModelIndex(), QModelIndex())
|
|
78
88
|
|
|
79
89
|
def child_count(self):
|
|
80
90
|
"""Return the number of children this node has"""
|
|
@@ -100,12 +110,23 @@ class ParamGroupItem(object):
|
|
|
100
110
|
class ParamBlockModel(QAbstractItemModel):
|
|
101
111
|
"""Model for handling the parameters in the tree-view"""
|
|
102
112
|
|
|
103
|
-
def __init__(self, parent):
|
|
113
|
+
def __init__(self, parent, mainUI):
|
|
104
114
|
"""Create the empty model"""
|
|
105
115
|
super(ParamBlockModel, self).__init__(parent)
|
|
106
116
|
self._nodes = []
|
|
107
|
-
self._column_headers = ['Name', 'Type', 'Access', 'Value']
|
|
117
|
+
self._column_headers = ['Name', 'Type', 'Access', 'Persistent', 'Value', 'Stored Value']
|
|
108
118
|
self._red_brush = QBrush(QColor("red"))
|
|
119
|
+
self._enabled = False
|
|
120
|
+
self._mainUI = mainUI
|
|
121
|
+
self.proxy = None
|
|
122
|
+
|
|
123
|
+
def set_proxy(self, proxy):
|
|
124
|
+
self.proxy = proxy
|
|
125
|
+
|
|
126
|
+
def set_enabled(self, enabled):
|
|
127
|
+
if self._enabled != enabled:
|
|
128
|
+
self._enabled = enabled
|
|
129
|
+
self.layoutChanged.emit()
|
|
109
130
|
|
|
110
131
|
def set_toc(self, toc, crazyflie):
|
|
111
132
|
"""Populate the model with data from the param TOC"""
|
|
@@ -114,9 +135,13 @@ class ParamBlockModel(QAbstractItemModel):
|
|
|
114
135
|
for group in sorted(toc.keys()):
|
|
115
136
|
new_group = ParamGroupItem(group, self)
|
|
116
137
|
for param in sorted(toc[group].keys()):
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
new_param
|
|
138
|
+
elem = toc[group][param]
|
|
139
|
+
is_persistent = elem.is_persistent()
|
|
140
|
+
new_param = ParamChildItem(new_group, param, is_persistent, crazyflie)
|
|
141
|
+
new_param.ctype = elem.ctype
|
|
142
|
+
new_param.access = elem.get_readable_access()
|
|
143
|
+
new_param.persistent = elem.is_persistent()
|
|
144
|
+
|
|
120
145
|
crazyflie.param.add_update_callback(
|
|
121
146
|
group=group, name=param, cb=new_param.updated)
|
|
122
147
|
new_group.children.append(new_param)
|
|
@@ -146,7 +171,7 @@ class ParamBlockModel(QAbstractItemModel):
|
|
|
146
171
|
|
|
147
172
|
def headerData(self, section, orientation, role):
|
|
148
173
|
"""Re-implemented method to get the headers"""
|
|
149
|
-
if role == Qt.DisplayRole:
|
|
174
|
+
if role == Qt.ItemDataRole.DisplayRole:
|
|
150
175
|
return self._column_headers[section]
|
|
151
176
|
|
|
152
177
|
def rowCount(self, parent):
|
|
@@ -173,12 +198,26 @@ class ParamBlockModel(QAbstractItemModel):
|
|
|
173
198
|
|
|
174
199
|
def data(self, index, role):
|
|
175
200
|
"""Re-implemented method to get the data for a given index and role"""
|
|
201
|
+
|
|
202
|
+
if role == Qt.ItemDataRole.BackgroundRole:
|
|
203
|
+
if index.row() % 2 == 0:
|
|
204
|
+
return QVariant(self._mainUI.bgColor)
|
|
205
|
+
else:
|
|
206
|
+
multiplier = 1.15 if self._mainUI.isDark else 0.95
|
|
207
|
+
return QVariant(
|
|
208
|
+
QColor(
|
|
209
|
+
int(self._mainUI.bgColor.red() * multiplier),
|
|
210
|
+
int(self._mainUI.bgColor.green() * multiplier),
|
|
211
|
+
int(self._mainUI.bgColor.blue() * multiplier)
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
176
215
|
node = index.internalPointer()
|
|
177
216
|
parent = node.parent
|
|
178
217
|
if not parent:
|
|
179
|
-
if role == Qt.DisplayRole and index.column() == 0:
|
|
218
|
+
if role == Qt.ItemDataRole.DisplayRole and index.column() == 0:
|
|
180
219
|
return node.name
|
|
181
|
-
elif role == Qt.DisplayRole:
|
|
220
|
+
elif role == Qt.ItemDataRole.DisplayRole:
|
|
182
221
|
if index.column() == 0:
|
|
183
222
|
return node.name
|
|
184
223
|
if index.column() == 1:
|
|
@@ -186,32 +225,78 @@ class ParamBlockModel(QAbstractItemModel):
|
|
|
186
225
|
if index.column() == 2:
|
|
187
226
|
return node.access
|
|
188
227
|
if index.column() == 3:
|
|
228
|
+
return 'Yes' if node.persistent else 'No'
|
|
229
|
+
if index.column() == 4:
|
|
189
230
|
return node.value
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
elif (role == Qt.BackgroundRole and index.column() ==
|
|
231
|
+
if index.column() == 5:
|
|
232
|
+
return node.stored_value
|
|
233
|
+
elif (role == Qt.ItemDataRole.BackgroundRole and index.column() == 4 and
|
|
193
234
|
node.is_updating):
|
|
194
235
|
return self._red_brush
|
|
195
236
|
|
|
196
237
|
return None
|
|
197
238
|
|
|
198
|
-
def
|
|
199
|
-
"""
|
|
200
|
-
node
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
239
|
+
def update_stored_value_and_refresh(self, node):
|
|
240
|
+
"""
|
|
241
|
+
Fetch the persistent stored value for this node, store it in node.stored_value,
|
|
242
|
+
and refresh the Stored Value column in the view.
|
|
243
|
+
"""
|
|
244
|
+
if not node.persistent:
|
|
245
|
+
node.stored_value = ''
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
# Fetch stored value synchronously
|
|
249
|
+
complete_name = f"{node.parent.name}.{node.name}"
|
|
250
|
+
from threading import Event
|
|
251
|
+
wait_event = Event()
|
|
252
|
+
state_value = None
|
|
253
|
+
|
|
254
|
+
def cb(_, state):
|
|
255
|
+
nonlocal state_value
|
|
256
|
+
state_value = state
|
|
257
|
+
wait_event.set()
|
|
258
|
+
|
|
259
|
+
self._mainUI.cf.param.persistent_get_state(complete_name, cb)
|
|
260
|
+
wait_event.wait(timeout=0.2)
|
|
261
|
+
|
|
262
|
+
if state_value and state_value.is_stored:
|
|
263
|
+
node.stored_value = round_if_float(state_value.stored_value)
|
|
264
|
+
else:
|
|
265
|
+
node.stored_value = ''
|
|
266
|
+
|
|
267
|
+
# Refresh only column 5 (Stored Value)
|
|
268
|
+
source_row = node.parent.children.index(node)
|
|
269
|
+
parent_row = self._nodes.index(node.parent)
|
|
270
|
+
col = 5
|
|
271
|
+
row_index = self.index(source_row, col, self.createIndex(parent_row, 0, node.parent))
|
|
272
|
+
proxy_index = self.proxy.mapFromSource(row_index)
|
|
273
|
+
self.proxy.dataChanged.emit(proxy_index, proxy_index)
|
|
274
|
+
|
|
275
|
+
def update_stored_value_from_state(self, node, state: 'PersistentParamState'):
|
|
276
|
+
"""
|
|
277
|
+
Update the node's stored_value from a PersistentParamState object
|
|
278
|
+
and refresh the Stored Value column.
|
|
279
|
+
"""
|
|
280
|
+
if state and state.is_stored:
|
|
281
|
+
node.stored_value = round_if_float(state.stored_value)
|
|
282
|
+
else:
|
|
283
|
+
node.stored_value = ''
|
|
284
|
+
|
|
285
|
+
# Refresh only column 5 (Stored Value)
|
|
286
|
+
source_row = node.parent.children.index(node)
|
|
287
|
+
parent_row = self._nodes.index(node.parent)
|
|
288
|
+
col = 5 # Stored Value column
|
|
289
|
+
row_index = self.index(source_row, col, self.createIndex(parent_row, 0, node.parent))
|
|
290
|
+
proxy_index = self.proxy.mapFromSource(row_index)
|
|
291
|
+
self.proxy.dataChanged.emit(proxy_index, proxy_index)
|
|
208
292
|
|
|
209
293
|
def flags(self, index):
|
|
210
294
|
"""Re-implemented function for getting the flags for a certain index"""
|
|
211
295
|
flag = super(ParamBlockModel, self).flags(index)
|
|
212
|
-
|
|
213
|
-
if
|
|
214
|
-
|
|
296
|
+
|
|
297
|
+
if not self._enabled:
|
|
298
|
+
return Qt.ItemFlag.NoItemFlags
|
|
299
|
+
|
|
215
300
|
return flag
|
|
216
301
|
|
|
217
302
|
def reset(self):
|
|
@@ -222,7 +307,24 @@ class ParamBlockModel(QAbstractItemModel):
|
|
|
222
307
|
self.layoutChanged.emit()
|
|
223
308
|
|
|
224
309
|
|
|
225
|
-
class
|
|
310
|
+
class ParamTreeFilterProxy(QSortFilterProxyModel):
|
|
311
|
+
"""
|
|
312
|
+
Implement a filtering proxy model that show all children if the group matches.
|
|
313
|
+
"""
|
|
314
|
+
def __init__(self, paramTree):
|
|
315
|
+
super(ParamTreeFilterProxy, self).__init__(paramTree)
|
|
316
|
+
|
|
317
|
+
def filterAcceptsRow(self, source_row: int, source_parent: QModelIndex) -> bool:
|
|
318
|
+
'''
|
|
319
|
+
When a group match the filter, make sure all children matches as well.
|
|
320
|
+
'''
|
|
321
|
+
if not source_parent.isValid():
|
|
322
|
+
return super().filterAcceptsRow(source_row, source_parent)
|
|
323
|
+
|
|
324
|
+
return super().filterAcceptsRow(source_parent.row(), source_parent.parent())
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class ParamTab(TabToolbox, param_tab_class):
|
|
226
328
|
"""
|
|
227
329
|
Show all the parameters in the TOC and give the user the ability to edit
|
|
228
330
|
them
|
|
@@ -231,16 +333,16 @@ class ParamTab(Tab, param_tab_class):
|
|
|
231
333
|
_connected_signal = pyqtSignal(str)
|
|
232
334
|
_disconnected_signal = pyqtSignal(str)
|
|
233
335
|
|
|
234
|
-
|
|
336
|
+
_set_param_value_signal = pyqtSignal()
|
|
337
|
+
_persistent_state_signal = pyqtSignal(PersistentParamState)
|
|
338
|
+
_param_default_signal = pyqtSignal(object)
|
|
339
|
+
_reset_param_signal = pyqtSignal(str)
|
|
340
|
+
|
|
341
|
+
def __init__(self, helper):
|
|
235
342
|
"""Create the parameter tab"""
|
|
236
|
-
super(ParamTab, self).__init__(
|
|
343
|
+
super(ParamTab, self).__init__(helper, 'Parameters')
|
|
237
344
|
self.setupUi(self)
|
|
238
345
|
|
|
239
|
-
self.tabName = "Parameters"
|
|
240
|
-
self.menuName = "Parameters"
|
|
241
|
-
|
|
242
|
-
self.helper = helper
|
|
243
|
-
self.tabWidget = tabWidget
|
|
244
346
|
self.cf = helper.cf
|
|
245
347
|
|
|
246
348
|
self.cf.connected.add_callback(self._connected_signal.emit)
|
|
@@ -248,14 +350,298 @@ class ParamTab(Tab, param_tab_class):
|
|
|
248
350
|
self.cf.disconnected.add_callback(self._disconnected_signal.emit)
|
|
249
351
|
self._disconnected_signal.connect(self._disconnected)
|
|
250
352
|
|
|
251
|
-
self._model = ParamBlockModel(None)
|
|
252
|
-
self.
|
|
353
|
+
self._model = ParamBlockModel(None, self._helper.mainUI)
|
|
354
|
+
self._persistent_state_signal.connect(self._persistent_state_cb)
|
|
355
|
+
self._set_param_value_signal.connect(self._set_param_value)
|
|
356
|
+
self.setParamButton.clicked.connect(self._set_param_value_signal.emit)
|
|
357
|
+
self.currentValue.returnPressed.connect(self._set_param_value_signal.emit)
|
|
358
|
+
self._param_default_signal.connect(self._param_default_cb)
|
|
359
|
+
|
|
360
|
+
self._reset_param_signal.connect(lambda text: self.currentValue.setText(text))
|
|
361
|
+
self.resetDefaultButton.clicked.connect(lambda: self._reset_param_signal.emit(self.defaultValue.text()))
|
|
362
|
+
self.persistentButton.clicked.connect(self._persistent_button_cb)
|
|
363
|
+
|
|
364
|
+
self.proxyModel = ParamTreeFilterProxy(self.paramTree)
|
|
365
|
+
self.proxyModel.setSourceModel(self._model)
|
|
366
|
+
self.proxyModel.setRecursiveFilteringEnabled(True)
|
|
367
|
+
self._model.set_proxy(self.proxyModel)
|
|
368
|
+
|
|
369
|
+
@QtCore.pyqtSlot(str)
|
|
370
|
+
def onFilterChanged(text):
|
|
371
|
+
self.proxyModel.setFilterRegExp(text)
|
|
372
|
+
|
|
373
|
+
self.filterBox.textChanged.connect(onFilterChanged)
|
|
374
|
+
|
|
375
|
+
self.paramTree.setModel(self.proxyModel)
|
|
376
|
+
self.paramTree.header().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
|
377
|
+
self.paramTree.selectionModel().selectionChanged.connect(self._paramChanged)
|
|
378
|
+
|
|
379
|
+
self._load_param_button.clicked.connect(self._load_param_button_clicked)
|
|
380
|
+
self._dump_param_button.clicked.connect(self._dump_param_button_clicked)
|
|
381
|
+
self._clear_param_button.clicked.connect(self._clear_stored_persistent_params_button_clicked)
|
|
382
|
+
|
|
383
|
+
self._is_connected = False
|
|
384
|
+
self._update_param_io_buttons()
|
|
385
|
+
|
|
386
|
+
def _param_default_cb(self, default_value):
|
|
387
|
+
if default_value is not None:
|
|
388
|
+
self.defaultValue.setText(str(default_value))
|
|
389
|
+
else:
|
|
390
|
+
self.defaultValue.setText('-')
|
|
391
|
+
|
|
392
|
+
def _persistent_button_cb(self, _):
|
|
393
|
+
def success_cb(name, success):
|
|
394
|
+
print(f'store {success}!')
|
|
395
|
+
if success:
|
|
396
|
+
# Fetch the persistent state after store
|
|
397
|
+
def state_cb(_, state):
|
|
398
|
+
for group in self._model._nodes:
|
|
399
|
+
for node in group.children:
|
|
400
|
+
if f"{group.name}.{node.name}" == name:
|
|
401
|
+
# Update stored value in model
|
|
402
|
+
self._model.update_stored_value_from_state(node, state)
|
|
403
|
+
# Update the button text immediately
|
|
404
|
+
self._persistent_state_signal.emit(state)
|
|
405
|
+
break
|
|
406
|
+
self.cf.param.persistent_get_state(name, state_cb)
|
|
407
|
+
|
|
408
|
+
complete = self.paramDetailsLabel.text()
|
|
409
|
+
if self.persistentButton.text() == 'Clear':
|
|
410
|
+
self.cf.param.persistent_clear(complete, success_cb)
|
|
411
|
+
else:
|
|
412
|
+
self.cf.param.persistent_store(complete, success_cb)
|
|
413
|
+
|
|
414
|
+
def _persistent_state_cb(self, state):
|
|
415
|
+
print(f'persistent callback! state: {state}')
|
|
416
|
+
self.persistentFrame.setVisible(True)
|
|
417
|
+
|
|
418
|
+
if state.is_stored:
|
|
419
|
+
self.storedValue.setText(str(state.stored_value))
|
|
420
|
+
else:
|
|
421
|
+
self.storedValue.setText('Not stored')
|
|
422
|
+
|
|
423
|
+
self.persistentButton.setText('Clear' if state.is_stored else 'Store')
|
|
424
|
+
|
|
425
|
+
def _set_param_value(self):
|
|
426
|
+
name = self.paramDetailsLabel.text()
|
|
427
|
+
value = self.currentValue.text()
|
|
428
|
+
try:
|
|
429
|
+
self.cf.param.set_value(name, value)
|
|
430
|
+
self.currentValue.setStyleSheet('')
|
|
431
|
+
except Exception:
|
|
432
|
+
self.currentValue.setStyleSheet('border: 1px solid red')
|
|
433
|
+
|
|
434
|
+
def _paramChanged(self):
|
|
435
|
+
|
|
436
|
+
group = None
|
|
437
|
+
param = None
|
|
438
|
+
indexes = self.paramTree.selectionModel().selectedIndexes()
|
|
439
|
+
if len(indexes) > 0:
|
|
440
|
+
selectedIndex = indexes[0]
|
|
441
|
+
if selectedIndex.parent().isValid():
|
|
442
|
+
group = selectedIndex.parent().data()
|
|
443
|
+
param = selectedIndex.data()
|
|
444
|
+
else:
|
|
445
|
+
group = selectedIndex.data()
|
|
446
|
+
|
|
447
|
+
# Made visible in _persistent_state_cb()
|
|
448
|
+
self.persistentFrame.setVisible(False)
|
|
449
|
+
|
|
450
|
+
are_details_visible = param is not None
|
|
451
|
+
self.valueFrame.setVisible(are_details_visible)
|
|
452
|
+
self.paramDetailsLabel.setVisible(are_details_visible)
|
|
453
|
+
self.paramDetailsDescription.setVisible(are_details_visible)
|
|
454
|
+
|
|
455
|
+
if param:
|
|
456
|
+
self.paramDetailsLabel.setText(f'{group}.{param}' if param is not None else group)
|
|
457
|
+
if cfclient.log_param_doc is not None:
|
|
458
|
+
try:
|
|
459
|
+
desc = str()
|
|
460
|
+
group_doc = cfclient.log_param_doc['params'][group]
|
|
461
|
+
if param is None:
|
|
462
|
+
desc = group_doc['desc']
|
|
463
|
+
else:
|
|
464
|
+
desc = group_doc['variables'][param]['short_desc']
|
|
465
|
+
|
|
466
|
+
self.paramDetailsDescription.setWordWrap(True)
|
|
467
|
+
self.paramDetailsDescription.setText(desc.replace('\n', ''))
|
|
468
|
+
except (KeyError, TypeError, AttributeError):
|
|
469
|
+
self.paramDetailsDescription.setText('')
|
|
470
|
+
|
|
471
|
+
complete = f'{group}.{param}'
|
|
472
|
+
elem = self.cf.param.toc.get_element_by_complete_name(complete)
|
|
473
|
+
value = round_if_float(self.cf.param.get_value(complete))
|
|
474
|
+
self.currentValue.setText(value)
|
|
475
|
+
self.currentValue.setStyleSheet('')
|
|
476
|
+
self.currentValue.setCursorPosition(0)
|
|
477
|
+
self.defaultValue.setText('-')
|
|
478
|
+
self.cf.param.get_default_value(complete, lambda _, value: self._param_default_signal.emit(value))
|
|
479
|
+
|
|
480
|
+
writable = elem.get_readable_access() == 'RW'
|
|
481
|
+
self.currentValue.setEnabled(writable)
|
|
482
|
+
self.setParamButton.setEnabled(writable)
|
|
483
|
+
self.resetDefaultButton.setEnabled(writable)
|
|
484
|
+
|
|
485
|
+
if elem.is_persistent():
|
|
486
|
+
self.cf.param.persistent_get_state(complete, lambda _, state: self._persistent_state_signal.emit(state))
|
|
487
|
+
source_index = self.proxyModel.mapToSource(indexes[0])
|
|
488
|
+
node = source_index.internalPointer()
|
|
489
|
+
self._model.update_stored_value_and_refresh(node)
|
|
490
|
+
|
|
491
|
+
def _update_param_io_buttons(self):
|
|
492
|
+
enabled = self._is_connected
|
|
493
|
+
self._load_param_button.setEnabled(enabled)
|
|
494
|
+
self._dump_param_button.setEnabled(enabled)
|
|
495
|
+
self._clear_param_button.setEnabled(enabled)
|
|
496
|
+
|
|
497
|
+
def _load_param_button_clicked(self):
|
|
498
|
+
names = QFileDialog.getOpenFileName(self, 'Open file', cfclient.config_path, FILE_REGEX_YAML)
|
|
499
|
+
|
|
500
|
+
if names[0] == '':
|
|
501
|
+
return
|
|
502
|
+
filename = names[0]
|
|
503
|
+
parameters = ParamFileManager.read(filename)
|
|
504
|
+
|
|
505
|
+
def _is_persistent_stored_callback(complete_name, success):
|
|
506
|
+
if not success:
|
|
507
|
+
print(f'Persistent params: failed to store {complete_name}!')
|
|
508
|
+
QMessageBox.about(self, 'Warning', f'Failed to persistently store {complete_name}!')
|
|
509
|
+
else:
|
|
510
|
+
print(f'Persistent params: stored {complete_name}!')
|
|
511
|
+
|
|
512
|
+
_set_param_names = []
|
|
513
|
+
for param, state in parameters.items():
|
|
514
|
+
if state.is_stored:
|
|
515
|
+
try:
|
|
516
|
+
self.cf.param.set_value(param, state.stored_value)
|
|
517
|
+
_set_param_names.append(param)
|
|
518
|
+
except Exception:
|
|
519
|
+
print(f'Failed to set {param}!')
|
|
520
|
+
QMessageBox.about(self, 'Warning', f'Failed to set {param}!')
|
|
521
|
+
print(f'Set {param}!')
|
|
522
|
+
self.cf.param.persistent_store(param, _is_persistent_stored_callback)
|
|
523
|
+
|
|
524
|
+
self._update_param_io_buttons()
|
|
525
|
+
dlg = QMessageBox(self)
|
|
526
|
+
dlg.setWindowTitle("Info")
|
|
527
|
+
_parameters_and_values = [f"{_param_name}:{parameters[_param_name].stored_value}"
|
|
528
|
+
for _param_name in _set_param_names]
|
|
529
|
+
dlg.setText('Loaded persistent parameters from file:\n' + "\n".join(_parameters_and_values))
|
|
530
|
+
dlg.setIcon(QMessageBox.Icon.NoIcon)
|
|
531
|
+
dlg.exec()
|
|
532
|
+
|
|
533
|
+
def _get_persistent_state(self, complete_param_name):
|
|
534
|
+
wait_for_callback_event = Event()
|
|
535
|
+
state_value = None
|
|
536
|
+
|
|
537
|
+
def state_callback(complete_name, value):
|
|
538
|
+
nonlocal state_value
|
|
539
|
+
state_value = value
|
|
540
|
+
wait_for_callback_event.set()
|
|
541
|
+
|
|
542
|
+
self.cf.param.persistent_get_state(complete_param_name, state_callback)
|
|
543
|
+
wait_for_callback_event.wait()
|
|
544
|
+
return state_value
|
|
545
|
+
|
|
546
|
+
def _get_all_persistent_param_names(self):
|
|
547
|
+
persistent_params = []
|
|
548
|
+
for group_name, params in self.cf.param.toc.toc.items():
|
|
549
|
+
for param_name, element in params.items():
|
|
550
|
+
if element.is_persistent():
|
|
551
|
+
complete_name = group_name + '.' + param_name
|
|
552
|
+
persistent_params.append(complete_name)
|
|
553
|
+
|
|
554
|
+
return persistent_params
|
|
555
|
+
|
|
556
|
+
def _get_all_stored_persistent_param_names(self):
|
|
557
|
+
persistent_params = self._get_all_persistent_param_names()
|
|
558
|
+
stored_params = []
|
|
559
|
+
for complete_name in persistent_params:
|
|
560
|
+
state = self._get_persistent_state(complete_name)
|
|
561
|
+
if state.is_stored:
|
|
562
|
+
stored_params.append(complete_name)
|
|
563
|
+
return stored_params
|
|
564
|
+
|
|
565
|
+
def _get_all_stored_persistent_params(self):
|
|
566
|
+
persistent_params = self._get_all_persistent_param_names()
|
|
567
|
+
stored_params = {}
|
|
568
|
+
for complete_name in persistent_params:
|
|
569
|
+
state = self._get_persistent_state(complete_name)
|
|
570
|
+
if state.is_stored:
|
|
571
|
+
stored_params[complete_name] = state
|
|
572
|
+
return stored_params
|
|
573
|
+
|
|
574
|
+
def _dump_param_button_clicked(self):
|
|
575
|
+
stored_persistent_params = self._get_all_stored_persistent_params()
|
|
576
|
+
names = QFileDialog.getSaveFileName(self, 'Save file', cfclient.config_path, FILE_REGEX_YAML)
|
|
577
|
+
if names[0] == '':
|
|
578
|
+
return
|
|
579
|
+
if not names[0].endswith(".yaml"):
|
|
580
|
+
filename = names[0] + ".yaml"
|
|
581
|
+
else:
|
|
582
|
+
filename = names[0]
|
|
583
|
+
|
|
584
|
+
ParamFileManager.write(filename, stored_persistent_params)
|
|
585
|
+
dlg = QMessageBox(self)
|
|
586
|
+
dlg.setWindowTitle('Info')
|
|
587
|
+
_parameters_and_values = [f"{_param_name}: {stored_persistent_params[_param_name].stored_value}"
|
|
588
|
+
for _param_name in stored_persistent_params.keys()]
|
|
589
|
+
dlg.setText('Dumped persistent parameters to file:\n' + "\n".join(_parameters_and_values))
|
|
590
|
+
dlg.setIcon(QMessageBox.Icon.NoIcon)
|
|
591
|
+
dlg.exec()
|
|
592
|
+
|
|
593
|
+
def _clear_persistent_parameter(self, complete_param_name):
|
|
594
|
+
wait_for_callback_event = Event()
|
|
595
|
+
|
|
596
|
+
def is_stored_cleared(complete_name, success):
|
|
597
|
+
if success:
|
|
598
|
+
print(f'Persistent params: cleared {complete_name}!')
|
|
599
|
+
else:
|
|
600
|
+
print(f'Persistent params: failed to clear {complete_name}!')
|
|
601
|
+
wait_for_callback_event.set()
|
|
602
|
+
|
|
603
|
+
self.cf.param.persistent_clear(complete_param_name, callback=is_stored_cleared)
|
|
604
|
+
wait_for_callback_event.wait()
|
|
605
|
+
|
|
606
|
+
def _clear_stored_persistent_params_button_clicked(self):
|
|
607
|
+
dlg = QMessageBox(self)
|
|
608
|
+
dlg.setWindowTitle("Clear Stored Parameters Confirmation")
|
|
609
|
+
dlg.setText("Are you sure you want to clear your stored persistent parameters?")
|
|
610
|
+
dlg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
611
|
+
button = dlg.exec()
|
|
612
|
+
|
|
613
|
+
if button == QMessageBox.StandardButton.Yes:
|
|
614
|
+
stored_persistent_params = self._get_all_stored_persistent_param_names()
|
|
615
|
+
for complete_name in stored_persistent_params:
|
|
616
|
+
self._clear_persistent_parameter(complete_name)
|
|
617
|
+
for group in self._model._nodes:
|
|
618
|
+
for node in group.children:
|
|
619
|
+
if f"{group.name}.{node.name}" == complete_name:
|
|
620
|
+
node.stored_value = ''
|
|
621
|
+
# Emit a targeted dataChanged for column 5 only
|
|
622
|
+
source_row = group.children.index(node)
|
|
623
|
+
parent_row = self._model._nodes.index(group)
|
|
624
|
+
col = 5
|
|
625
|
+
|
|
626
|
+
index = self._model.index(
|
|
627
|
+
source_row, col,
|
|
628
|
+
self._model.index(parent_row, 0, QModelIndex())
|
|
629
|
+
)
|
|
630
|
+
proxy_index = self._model.proxy.mapFromSource(index)
|
|
631
|
+
self._model.proxy.dataChanged.emit(proxy_index, proxy_index)
|
|
632
|
+
break
|
|
253
633
|
|
|
254
634
|
def _connected(self, link_uri):
|
|
255
|
-
self._model.
|
|
256
|
-
self.
|
|
635
|
+
self._model.reset()
|
|
636
|
+
self._model.set_toc(self.cf.param.toc.toc, self._helper.cf)
|
|
637
|
+
self._model.set_enabled(True)
|
|
638
|
+
self._helper.cf.param.request_update_of_all_params()
|
|
639
|
+
self._is_connected = True
|
|
640
|
+
self._update_param_io_buttons()
|
|
257
641
|
|
|
258
642
|
def _disconnected(self, link_uri):
|
|
259
|
-
self.
|
|
643
|
+
self._is_connected = False
|
|
644
|
+
self._update_param_io_buttons()
|
|
260
645
|
self._model.reset()
|
|
261
|
-
self.
|
|
646
|
+
self._paramChanged()
|
|
647
|
+
self._model.set_enabled(False)
|