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.
Files changed (140) hide show
  1. cfclient/__init__.py +16 -11
  2. cfclient/configs/config.json +4 -3
  3. cfclient/configs/input/Generic_OS_X.json +1 -0
  4. cfclient/configs/input/Joystick.json +1 -0
  5. cfclient/configs/input/PS3_Mode_1.json +1 -0
  6. cfclient/configs/input/PS3_Mode_2.json +1 -0
  7. cfclient/configs/input/PS3_Mode_3.json +1 -0
  8. cfclient/configs/input/PS4_Mode_1.json +1 -0
  9. cfclient/configs/input/PS4_Mode_2.json +1 -0
  10. cfclient/configs/input/PS4_shoulder_btns_yaw.json +1 -0
  11. cfclient/configs/input/xbox360_mode1.json +1 -0
  12. cfclient/configs/log/PID_tuning/Attitude.json +46 -0
  13. cfclient/configs/log/PID_tuning/Attitude_rate.json +46 -0
  14. cfclient/configs/log/PID_tuning/Position.json +46 -0
  15. cfclient/configs/log/PID_tuning/Velocity.json +46 -0
  16. cfclient/configs/log/PID_tuning_components/Pitch.json +22 -0
  17. cfclient/configs/log/PID_tuning_components/Pitch_rate.json +22 -0
  18. cfclient/configs/log/PID_tuning_components/Position_x.json +22 -0
  19. cfclient/configs/log/PID_tuning_components/Position_y.json +22 -0
  20. cfclient/configs/log/PID_tuning_components/Position_z.json +22 -0
  21. cfclient/configs/log/PID_tuning_components/Roll.json +22 -0
  22. cfclient/configs/log/PID_tuning_components/Roll_rate.json +22 -0
  23. cfclient/configs/log/PID_tuning_components/Velocity_x.json +22 -0
  24. cfclient/configs/log/PID_tuning_components/Velocity_y.json +22 -0
  25. cfclient/configs/log/PID_tuning_components/Velocity_z.json +22 -0
  26. cfclient/configs/log/PID_tuning_components/Yaw.json +22 -0
  27. cfclient/configs/log/PID_tuning_components/Yaw_rate.json +22 -0
  28. cfclient/gui.py +44 -9
  29. cfclient/headless.py +3 -12
  30. cfclient/resources/log_param_doc.json +1 -0
  31. cfclient/ui/connectivity_manager.py +198 -0
  32. cfclient/ui/dialogs/about.py +53 -36
  33. cfclient/ui/dialogs/about.ui +23 -3
  34. cfclient/ui/dialogs/anchor_position_dialog.py +252 -0
  35. cfclient/ui/dialogs/anchor_position_dialog.ui +138 -0
  36. cfclient/ui/dialogs/basestation_mode_dialog.py +185 -0
  37. cfclient/ui/dialogs/basestation_mode_dialog.ui +186 -0
  38. cfclient/ui/dialogs/bootloader.py +448 -85
  39. cfclient/ui/dialogs/bootloader.ui +387 -134
  40. cfclient/ui/dialogs/cf2config.py +4 -4
  41. cfclient/ui/dialogs/cf2config.ui +3 -4
  42. cfclient/ui/dialogs/inputconfigdialogue.py +24 -19
  43. cfclient/ui/dialogs/inputconfigdialogue.ui +53 -30
  44. cfclient/ui/dialogs/lighthouse_bs_geometry_dialog.py +220 -0
  45. cfclient/ui/dialogs/lighthouse_bs_geometry_dialog.ui +110 -0
  46. cfclient/ui/dialogs/lighthouse_system_type_dialog.py +93 -0
  47. cfclient/ui/dialogs/lighthouse_system_type_dialog.ui +121 -0
  48. cfclient/ui/dialogs/logconfigdialogue.py +401 -101
  49. cfclient/ui/dialogs/logconfigdialogue.ui +117 -72
  50. cfclient/ui/icons/bl.webp +0 -0
  51. cfclient/ui/icons/bolt.webp +0 -0
  52. cfclient/ui/icons/cf21.webp +0 -0
  53. cfclient/ui/icons/checkmark_black.png +0 -0
  54. cfclient/ui/icons/checkmark_white.png +0 -0
  55. cfclient/ui/icons/create.png +0 -0
  56. cfclient/ui/icons/delete.png +0 -0
  57. cfclient/ui/icons/flapper.webp +0 -0
  58. cfclient/ui/icons/tag.webp +0 -0
  59. cfclient/ui/main.py +328 -258
  60. cfclient/ui/main.ui +184 -80
  61. cfclient/ui/pluginhelper.py +7 -1
  62. cfclient/ui/pose_logger.py +116 -0
  63. cfclient/ui/tab_toolbox.py +208 -0
  64. cfclient/ui/tabs/ColorLEDTab.py +752 -0
  65. cfclient/ui/tabs/ConsoleTab.py +48 -13
  66. cfclient/ui/{toolboxes → tabs}/CrtpSharkToolbox.py +19 -34
  67. cfclient/ui/tabs/ExampleTab.py +9 -16
  68. cfclient/ui/tabs/FlightTab.py +437 -325
  69. cfclient/ui/tabs/GpsTab.py +14 -20
  70. cfclient/ui/tabs/LEDRingTab.py +277 -0
  71. cfclient/ui/tabs/LogBlockDebugTab.py +20 -27
  72. cfclient/ui/tabs/LogBlockTab.py +35 -35
  73. cfclient/ui/tabs/LogClientTab.py +85 -0
  74. cfclient/ui/tabs/LogTab.py +50 -27
  75. cfclient/ui/tabs/ParamTab.py +443 -57
  76. cfclient/ui/tabs/PlotTab.py +23 -25
  77. cfclient/ui/tabs/TuningTab.py +292 -0
  78. cfclient/ui/tabs/__init__.py +12 -2
  79. cfclient/ui/tabs/colorLEDTab.ui +624 -0
  80. cfclient/ui/tabs/consoleTab.ui +46 -0
  81. cfclient/ui/tabs/flightActionContainer.ui +103 -0
  82. cfclient/ui/tabs/flightTab.ui +724 -237
  83. cfclient/ui/tabs/{ledTab.ui → ledRingTab.ui} +63 -46
  84. cfclient/ui/tabs/lighthouse_tab.py +714 -0
  85. cfclient/ui/tabs/lighthouse_tab.ui +430 -0
  86. cfclient/ui/tabs/locopositioning_tab.py +606 -389
  87. cfclient/ui/tabs/locopositioning_tab.ui +370 -253
  88. cfclient/ui/tabs/logClientTab.ui +52 -0
  89. cfclient/ui/tabs/logTab.ui +1 -1
  90. cfclient/ui/tabs/paramTab.ui +204 -3
  91. cfclient/ui/tabs/tuningTab.ui +773 -0
  92. cfclient/ui/widgets/ai.py +37 -39
  93. cfclient/ui/widgets/hexspinbox.py +16 -10
  94. cfclient/ui/widgets/plotter.ui +39 -47
  95. cfclient/ui/widgets/plotwidget.py +57 -22
  96. cfclient/ui/widgets/super_slider.py +112 -0
  97. cfclient/ui/wizards/__init__.py +0 -0
  98. cfclient/ui/wizards/bslh_1.png +0 -0
  99. cfclient/ui/wizards/bslh_2.png +0 -0
  100. cfclient/ui/wizards/bslh_3.png +0 -0
  101. cfclient/ui/wizards/bslh_4.png +0 -0
  102. cfclient/ui/wizards/bslh_5.png +0 -0
  103. cfclient/ui/wizards/lighthouse_geo_bs_estimation_wizard.py +465 -0
  104. cfclient/utils/config_manager.py +5 -4
  105. cfclient/utils/input/__init__.py +77 -19
  106. cfclient/utils/input/inputinterfaces/wiimote.py +2 -2
  107. cfclient/utils/input/inputreaderinterface.py +17 -7
  108. cfclient/utils/input/inputreaders/__init__.py +17 -0
  109. cfclient/utils/logconfigreader.py +245 -25
  110. cfclient/utils/logdatawriter.py +3 -1
  111. cfclient/utils/periodictimer.py +1 -1
  112. cfclient/utils/ui.py +336 -0
  113. cfclient/utils/zmq_led_driver.py +5 -0
  114. cfclient/utils/zmq_param.py +6 -0
  115. cfclient/version.py +34 -1
  116. cfclient-2025.12.1.dist-info/METADATA +70 -0
  117. cfclient-2025.12.1.dist-info/RECORD +152 -0
  118. {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/WHEEL +1 -1
  119. {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/entry_points.txt +0 -1
  120. cfclient-2025.12.1.dist-info/licenses/LICENSE.txt +350 -0
  121. {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/top_level.txt +1 -0
  122. cfconfig/Makefile +51 -0
  123. cfconfig/configblock.py +111 -0
  124. cfloader/__init__.py +41 -55
  125. cfzmq/__init__.py +22 -14
  126. cfclient/ui/dialogs/cf1config.py +0 -265
  127. cfclient/ui/dialogs/cf1config.ui +0 -260
  128. cfclient/ui/tab.py +0 -96
  129. cfclient/ui/tabs/LEDTab.py +0 -169
  130. cfclient/ui/toolboxes/ConsoleToolbox.py +0 -69
  131. cfclient/ui/toolboxes/DebugDriverToolbox.py +0 -107
  132. cfclient/ui/toolboxes/__init__.py +0 -45
  133. cfclient/ui/toolboxes/consoleToolbox.ui +0 -62
  134. cfclient/ui/toolboxes/debugDriverToolbox.ui +0 -86
  135. cfclient-2017.4.dist-info/DESCRIPTION.rst +0 -3
  136. cfclient-2017.4.dist-info/METADATA +0 -22
  137. cfclient-2017.4.dist-info/RECORD +0 -104
  138. cfclient-2017.4.dist-info/metadata.json +0 -1
  139. /cfclient/{icon-256.png → ui/icons/icon-256.png} +0 -0
  140. /cfclient/ui/{toolboxes → tabs}/crtpSharkToolbox.ui +0 -0
@@ -7,7 +7,7 @@
7
7
  # +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
8
8
  # || || /_____/_/\__/\___/_/ \__,_/ /___/\___/
9
9
  #
10
- # Copyright (C) 2011-2013 Bitcraze AB
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 PyQt5 import uic
36
- from PyQt5.QtCore import Qt, pyqtSignal
37
- from PyQt5.QtCore import QAbstractItemModel, QModelIndex
38
- from PyQt5.QtGui import QBrush, QColor
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.tab import Tab
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.refresh()
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
- new_param = ParamChildItem(new_group, param, crazyflie)
118
- new_param.ctype = toc[group][param].ctype
119
- new_param.access = toc[group][param].get_readable_access()
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
- elif role == Qt.EditRole and index.column() == 3:
191
- return node.value
192
- elif (role == Qt.BackgroundRole and index.column() == 3 and
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 setData(self, index, value, role):
199
- """Re-implemented function called when a value has been edited"""
200
- node = index.internalPointer()
201
- if role == Qt.EditRole:
202
- new_val = str(value)
203
- # This will not update the value, only trigger a setting and
204
- # reading of the parameter from the Crazyflie
205
- node.set_value(new_val)
206
- return True
207
- return False
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
- node = index.internalPointer()
213
- if index.column() == 3 and node.parent and node.access == "RW":
214
- flag |= Qt.ItemIsEditable
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 ParamTab(Tab, param_tab_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
- def __init__(self, tabWidget, helper, *args):
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__(*args)
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.paramTree.setModel(self._model)
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.set_toc(self.cf.param.toc.toc, self.helper.cf)
256
- self.paramTree.expandAll()
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._model.beginResetModel()
643
+ self._is_connected = False
644
+ self._update_param_io_buttons()
260
645
  self._model.reset()
261
- self._model.endResetModel()
646
+ self._paramChanged()
647
+ self._model.set_enabled(False)