cfclient 2024.7.1__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 (55) hide show
  1. cfclient/__init__.py +9 -7
  2. cfclient/configs/config.json +1 -1
  3. cfclient/configs/log/PID_tuning/Attitude.json +46 -0
  4. cfclient/configs/log/PID_tuning/Attitude_rate.json +46 -0
  5. cfclient/configs/log/PID_tuning/Position.json +46 -0
  6. cfclient/configs/log/PID_tuning/Velocity.json +46 -0
  7. cfclient/configs/log/PID_tuning_components/Pitch.json +22 -0
  8. cfclient/configs/log/PID_tuning_components/Pitch_rate.json +22 -0
  9. cfclient/configs/log/PID_tuning_components/Position_x.json +22 -0
  10. cfclient/configs/log/PID_tuning_components/Position_y.json +22 -0
  11. cfclient/configs/log/PID_tuning_components/Position_z.json +22 -0
  12. cfclient/configs/log/PID_tuning_components/Roll.json +22 -0
  13. cfclient/configs/log/PID_tuning_components/Roll_rate.json +22 -0
  14. cfclient/configs/log/PID_tuning_components/Velocity_x.json +22 -0
  15. cfclient/configs/log/PID_tuning_components/Velocity_y.json +22 -0
  16. cfclient/configs/log/PID_tuning_components/Velocity_z.json +22 -0
  17. cfclient/configs/log/PID_tuning_components/Yaw.json +22 -0
  18. cfclient/configs/log/PID_tuning_components/Yaw_rate.json +22 -0
  19. cfclient/resources/log_param_doc.json +1 -1
  20. cfclient/ui/dialogs/about.py +5 -1
  21. cfclient/ui/dialogs/basestation_mode_dialog.py +12 -1
  22. cfclient/ui/dialogs/bootloader.py +76 -6
  23. cfclient/ui/dialogs/bootloader.ui +110 -24
  24. cfclient/ui/dialogs/cf2config.ui +1 -1
  25. cfclient/ui/dialogs/lighthouse_bs_geometry_dialog.py +0 -12
  26. cfclient/ui/dialogs/lighthouse_bs_geometry_dialog.ui +0 -7
  27. cfclient/ui/icons/bl.webp +0 -0
  28. cfclient/ui/icons/bolt.webp +0 -0
  29. cfclient/ui/icons/cf21.webp +0 -0
  30. cfclient/ui/icons/flapper.webp +0 -0
  31. cfclient/ui/icons/tag.webp +0 -0
  32. cfclient/ui/main.py +3 -3
  33. cfclient/ui/main.ui +1 -1
  34. cfclient/ui/tabs/ColorLEDTab.py +752 -0
  35. cfclient/ui/tabs/FlightTab.py +11 -108
  36. cfclient/ui/tabs/{LEDTab.py → LEDRingTab.py} +126 -12
  37. cfclient/ui/tabs/LogTab.py +2 -2
  38. cfclient/ui/tabs/ParamTab.py +90 -4
  39. cfclient/ui/tabs/__init__.py +4 -2
  40. cfclient/ui/tabs/colorLEDTab.ui +624 -0
  41. cfclient/ui/tabs/consoleTab.ui +1 -1
  42. cfclient/ui/tabs/flightTab.ui +28 -71
  43. cfclient/ui/tabs/{ledTab.ui → ledRingTab.ui} +63 -46
  44. cfclient/utils/input/__init__.py +3 -3
  45. cfclient/utils/periodictimer.py +1 -1
  46. cfclient/version.py +34 -0
  47. cfclient-2025.12.1.dist-info/METADATA +70 -0
  48. {cfclient-2024.7.1.dist-info → cfclient-2025.12.1.dist-info}/RECORD +54 -28
  49. {cfclient-2024.7.1.dist-info → cfclient-2025.12.1.dist-info}/WHEEL +1 -1
  50. {cfclient-2024.7.1.dist-info → cfclient-2025.12.1.dist-info}/top_level.txt +1 -0
  51. cfconfig/Makefile +51 -0
  52. cfconfig/configblock.py +111 -0
  53. cfclient-2024.7.1.dist-info/METADATA +0 -53
  54. {cfclient-2024.7.1.dist-info → cfclient-2025.12.1.dist-info}/entry_points.txt +0 -0
  55. {cfclient-2024.7.1.dist-info → cfclient-2025.12.1.dist-info/licenses}/LICENSE.txt +0 -0
@@ -7,7 +7,7 @@
7
7
  # +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
8
8
  # || || /_____/_/\__/\___/_/ \__,_/ /___/\___/
9
9
  #
10
- # Copyright (C) 2011-2023 Bitcraze AB
10
+ # Copyright (C) 2011-2025 Bitcraze AB
11
11
  #
12
12
  # Crazyflie Nano Quadcopter Client
13
13
  #
@@ -138,7 +138,7 @@ class FlightTab(TabToolbox, flight_tab_class):
138
138
  self._emergency_stop_updated_signal.connect(self.updateEmergencyStop)
139
139
  self._helper.inputDeviceReader.emergency_stop_updated.add_callback(
140
140
  self._emergency_stop_updated_signal.emit)
141
- self._arm_updated_signal.connect(self.updateArm)
141
+ self._arm_updated_signal.connect(lambda: self.updateArm(from_controller=True))
142
142
  self._helper.inputDeviceReader.arm_updated.add_callback(self._arm_updated_signal.emit)
143
143
 
144
144
  self._helper.inputDeviceReader.heighthold_input_updated.add_callback(
@@ -195,17 +195,6 @@ class FlightTab(TabToolbox, flight_tab_class):
195
195
 
196
196
  self.uiSetupReady()
197
197
 
198
- self._led_ring_headlight.clicked.connect(
199
- lambda enabled: self._helper.cf.param.set_value("ring.headlightEnable", int(enabled)))
200
-
201
- self._helper.cf.param.add_update_callback(
202
- group="ring", name="headlightEnable",
203
- cb=(lambda name, checked: self._led_ring_headlight.setChecked(bool(int(checked)))))
204
-
205
- self._ledring_nbr_effects = 0
206
-
207
- self._helper.cf.param.add_update_callback(group="ring", name="effect", cb=self._ring_effect_updated)
208
-
209
198
  self._helper.cf.param.add_update_callback(group="imu_sensors", cb=self._set_available_sensors)
210
199
 
211
200
  self._helper.cf.param.all_updated.add_callback(self._all_params_updated)
@@ -219,10 +208,7 @@ class FlightTab(TabToolbox, flight_tab_class):
219
208
  self.targetCalPitch.setValue(Config().get("trim_pitch"))
220
209
  self.targetCalRoll.setValue(Config().get("trim_roll"))
221
210
 
222
- self._helper.inputDeviceReader.alt1_updated.add_callback(self.alt1_updated)
223
- self._helper.inputDeviceReader.alt2_updated.add_callback(self.alt2_updated)
224
211
  self._tf_state = 0
225
- self._ring_effect = 0
226
212
 
227
213
  # Connect callbacks for input device limiting of roll/pitch/yaw/thrust
228
214
  self._helper.inputDeviceReader.limiting_updated.add_callback(self._limiting_updated.emit)
@@ -446,7 +432,7 @@ class FlightTab(TabToolbox, flight_tab_class):
446
432
  # To prevent conflicting commands from the controller and the flight panel
447
433
  if JoystickReader().available_devices():
448
434
  self.commanderBox.setToolTip(
449
- 'Cant use both an controller and Command Based Flight'
435
+ 'Cannot use both a controller and Command Based Flight'
450
436
  )
451
437
  self.commanderBox.setEnabled(False)
452
438
  return
@@ -516,16 +502,6 @@ class FlightTab(TabToolbox, flight_tab_class):
516
502
  self._enable_estimators(False)
517
503
 
518
504
  self.logAltHold = None
519
- self._led_ring_effect.setEnabled(False)
520
- self._led_ring_effect.clear()
521
- try:
522
- self._led_ring_effect.currentIndexChanged.disconnect(
523
- self._ring_effect_changed)
524
- except TypeError:
525
- # Signal was not connected
526
- pass
527
- self._led_ring_effect.setCurrentIndex(-1)
528
- self._led_ring_headlight.setEnabled(False)
529
505
 
530
506
  try:
531
507
  self._assist_mode_combo.currentIndexChanged.disconnect(
@@ -638,19 +614,16 @@ class FlightTab(TabToolbox, flight_tab_class):
638
614
  else:
639
615
  self.setMotorLabelsEnabled(True)
640
616
 
641
- def updateArm(self):
642
- if self._is_flying():
617
+ def updateArm(self, from_controller=False):
618
+ if self._is_flying() and not from_controller:
643
619
  self._helper.cf.loc.send_emergency_stop()
644
- # TODO krri disarm?
645
- if self._is_crashed():
620
+ elif self._is_crashed():
646
621
  self._helper.cf.platform.send_crash_recovery_request()
647
- else:
648
- if self._is_armed():
649
- self._helper.cf.platform.send_arming_request(False)
650
- else:
651
- if self._can_arm():
652
- self.armButton.setStyleSheet("background-color: orange")
653
- self._helper.cf.platform.send_arming_request(True)
622
+ elif self._is_armed():
623
+ self._helper.cf.platform.send_arming_request(False)
624
+ elif self._can_arm():
625
+ self.armButton.setStyleSheet("background-color: orange")
626
+ self._helper.cf.platform.send_arming_request(True)
654
627
 
655
628
  def flightmodeChange(self, item):
656
629
  Config().set("flightmode", str(self.flightModeCombo.itemText(item)))
@@ -717,81 +690,11 @@ class FlightTab(TabToolbox, flight_tab_class):
717
690
  else:
718
691
  self._helper.cf.param.set_value("flightmode.althold", int(enabled))
719
692
 
720
- def alt1_updated(self, state):
721
- if state:
722
- new_index = (self._ring_effect+1) % (self._ledring_nbr_effects+1)
723
- self._helper.cf.param.set_value("ring.effect", str(new_index))
724
-
725
- def alt2_updated(self, state):
726
- self._helper.cf.param.set_value("ring.headlightEnable", str(state))
727
-
728
693
  def _all_params_updated(self):
729
- self._ring_populate_dropdown()
730
694
  self._populate_assisted_mode_dropdown()
731
695
  self._update_flight_commander(True)
732
696
  self._update_supervisor_and_arming(True)
733
697
 
734
- def _ring_populate_dropdown(self):
735
- try:
736
- nbr = int(self._helper.cf.param.values["ring"]["neffect"])
737
- current = int(self._helper.cf.param.values["ring"]["effect"])
738
- except KeyError:
739
- return
740
-
741
- # Used only in alt1_updated function
742
- self._ring_effect = current
743
- self._ledring_nbr_effects = nbr
744
-
745
- hardcoded_names = {
746
- 0: "Off",
747
- 1: "White spinner",
748
- 2: "Color spinner",
749
- 3: "Tilt effect",
750
- 4: "Brightness effect",
751
- 5: "Color spinner 2",
752
- 6: "Double spinner",
753
- 7: "Solid color effect",
754
- 8: "Factory test",
755
- 9: "Battery status",
756
- 10: "Boat lights",
757
- 11: "Alert",
758
- 12: "Gravity",
759
- 13: "LED tab",
760
- 14: "Color fader",
761
- 15: "Link quality",
762
- 16: "Location server status",
763
- 17: "Sequencer",
764
- 18: "Lighthouse quality",
765
- }
766
-
767
- for i in range(nbr + 1):
768
- name = "{}: ".format(i)
769
- if i in hardcoded_names:
770
- name += hardcoded_names[i]
771
- else:
772
- name += "N/A"
773
- self._led_ring_effect.addItem(name, i)
774
-
775
- self._led_ring_effect.currentIndexChanged.connect(
776
- self._ring_effect_changed)
777
-
778
- self._led_ring_effect.setCurrentIndex(current)
779
- if int(self._helper.cf.param.values["deck"]["bcLedRing"]) == 1:
780
- self._led_ring_effect.setEnabled(True)
781
- self._led_ring_headlight.setEnabled(True)
782
-
783
- def _ring_effect_changed(self, index):
784
- self._ring_effect = index
785
- if index > -1:
786
- i = self._led_ring_effect.itemData(index)
787
- logger.debug("Changed effect to {}".format(i))
788
- if i != int(self._helper.cf.param.values["ring"]["effect"]):
789
- self._helper.cf.param.set_value("ring.effect", str(i))
790
-
791
- def _ring_effect_updated(self, name, value):
792
- if self._helper.cf.param.is_updated:
793
- self._led_ring_effect.setCurrentIndex(int(value))
794
-
795
698
  def _populate_assisted_mode_dropdown(self):
796
699
  self._assist_mode_combo.addItem("Altitude hold", 0)
797
700
  self._assist_mode_combo.addItem("Position hold", 1)
@@ -7,7 +7,7 @@
7
7
  # +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
8
8
  # || || /_____/_/\__/\___/_/ \__,_/ /___/\___/
9
9
  #
10
- # Copyright (C) 2011-2023 Bitcraze AB
10
+ # Copyright (C) 2011-2025 Bitcraze AB
11
11
  #
12
12
  # Crazyflie Nano Quadcopter Client
13
13
  #
@@ -38,28 +38,47 @@ from PyQt6 import QtWidgets
38
38
 
39
39
  import cfclient
40
40
  from cfclient.ui.tab_toolbox import TabToolbox
41
- from cfclient.utils.ui import UiUtils
42
41
 
43
42
  from cflib.crazyflie.mem import MemoryElement
44
43
 
45
44
  __author__ = 'Bitcraze AB'
46
- __all__ = ['LEDTab']
45
+ __all__ = ['LEDRingTab']
47
46
 
48
47
  logger = logging.getLogger(__name__)
49
48
 
50
- led_tab_class = uic.loadUiType(cfclient.module_path + "/ui/tabs/ledTab.ui")[0]
49
+ led_ring_tab_class = uic.loadUiType(cfclient.module_path + "/ui/tabs/ledRingTab.ui")[0]
51
50
 
52
51
 
53
- class LEDTab(TabToolbox, led_tab_class):
52
+ class LEDRingTab(TabToolbox, led_ring_tab_class):
54
53
  """Tab for plotting logging data"""
55
54
 
56
55
  _connected_signal = pyqtSignal(str)
57
56
  _disconnected_signal = pyqtSignal(str)
58
57
 
59
58
  def __init__(self, helper):
60
- super(LEDTab, self).__init__(helper, 'LED')
59
+ super(LEDRingTab, self).__init__(helper, 'LED Ring')
61
60
  self.setupUi(self)
62
61
 
62
+ # LED-ring effect dropdown and headlight checkbox
63
+ self._ledring_nbr_effects = 0
64
+
65
+ # Connect the headlight checkbox
66
+ self._led_ring_headlight.clicked.connect(
67
+ lambda enabled: self._helper.cf.param.set_value("ring.headlightEnable", int(enabled)))
68
+
69
+ # Update headlight when param changes
70
+ self._helper.cf.param.add_update_callback(
71
+ group="ring", name="headlightEnable",
72
+ cb=lambda name, checked: self._led_ring_headlight.setChecked(bool(int(checked))))
73
+
74
+ # Update LED-ring effect when param changes
75
+ self._helper.cf.param.add_update_callback(
76
+ group="ring", name="effect",
77
+ cb=self._ring_effect_updated)
78
+
79
+ # Populate dropdown when all params are updated
80
+ self._helper.cf.param.all_updated.add_callback(self._ring_populate_dropdown)
81
+
63
82
  # Always wrap callbacks from Crazyflie API though QT Signal/Slots
64
83
  # to avoid manipulating the UI when rendering it
65
84
  self._connected_signal.connect(self._connected)
@@ -108,8 +127,14 @@ class LEDTab(TabToolbox, led_tab_class):
108
127
  self._intensity_spin.valueChanged.connect(
109
128
  self._intensity_slider.setValue)
110
129
 
130
+ self._helper.inputDeviceReader.alt1_updated.add_callback(self.alt1_updated)
131
+ self._helper.inputDeviceReader.alt2_updated.add_callback(self.alt2_updated)
132
+
133
+ self._led_ring_effect.setEnabled(False)
134
+ self._led_ring_headlight.setEnabled(False)
135
+
111
136
  def _select(self, nbr):
112
- col = QtGui.QColor() # default to invalid
137
+ col = QtGui.QColor()
113
138
 
114
139
  if self._mem:
115
140
  led = self._mem.leds[nbr]
@@ -118,10 +143,16 @@ class LEDTab(TabToolbox, led_tab_class):
118
143
  col = QtWidgets.QColorDialog.getColor(col)
119
144
 
120
145
  if col.isValid() and self._mem:
121
- logger.info(col.red())
122
- self._mem.leds[nbr].set(r=col.red(), g=col.green(), b=col.blue())
123
- UiUtils.set_background_color(self.sender(), col.red(), col.green(),
124
- col.blue())
146
+ r, g, b = col.red(), col.green(), col.blue()
147
+ self._mem.leds[nbr].set(r=r, g=g, b=b)
148
+
149
+ brightness = 0.299 * r + 0.587 * g + 0.114 * b
150
+ text_color = "white" if brightness < 128 else "black"
151
+
152
+ self.sender().setStyleSheet(
153
+ f"background-color: rgb({r}, {g}, {b}); color: {text_color};"
154
+ )
155
+
125
156
  self._write_led_output()
126
157
 
127
158
  def _intensity_change(self, value):
@@ -149,10 +180,13 @@ class LEDTab(TabToolbox, led_tab_class):
149
180
  if self._mem:
150
181
  for btn in self._btns:
151
182
  btn.setEnabled(True)
152
- btn.setStyleSheet("background-color: black")
183
+ btn.setStyleSheet("background-color: black; color: white")
153
184
  self._intensity_slider.setEnabled(True)
154
185
  self._intensity_spin.setEnabled(True)
155
186
 
187
+ self._led_ring_effect.setEnabled(True)
188
+ self._led_ring_headlight.setEnabled(True)
189
+
156
190
  def _disconnected(self, link_uri):
157
191
  """Callback for when the Crazyflie has been disconnected"""
158
192
  for btn in self._btns:
@@ -161,3 +195,83 @@ class LEDTab(TabToolbox, led_tab_class):
161
195
  self._intensity_slider.setEnabled(False)
162
196
  self._intensity_spin.setEnabled(False)
163
197
  self._intensity_slider.setValue(100)
198
+
199
+ self._led_ring_effect.setEnabled(False)
200
+ self._led_ring_headlight.setEnabled(False)
201
+
202
+ def _ring_populate_dropdown(self):
203
+ try:
204
+ nbr = int(self._helper.cf.param.values["ring"]["neffect"])
205
+ current = int(self._helper.cf.param.values["ring"]["effect"])
206
+ except KeyError:
207
+ return
208
+
209
+ self._ring_effect = current
210
+ self._ledring_nbr_effects = nbr
211
+
212
+ hardcoded_names = {
213
+ 0: "Off",
214
+ 1: "White spinner",
215
+ 2: "Color spinner",
216
+ 3: "Tilt effect",
217
+ 4: "Brightness effect",
218
+ 5: "Color spinner 2",
219
+ 6: "Double spinner",
220
+ 7: "Solid color effect",
221
+ 8: "Factory test",
222
+ 9: "Battery status",
223
+ 10: "Boat lights",
224
+ 11: "Alert",
225
+ 12: "Gravity",
226
+ 13: "LED tab",
227
+ 14: "Color fader",
228
+ 15: "Link quality",
229
+ 16: "Location server status",
230
+ 17: "Sequencer",
231
+ 18: "Lighthouse quality",
232
+ }
233
+
234
+ self._led_ring_effect.clear()
235
+ for i in range(nbr + 1):
236
+ name = "{}: ".format(i)
237
+ name += hardcoded_names.get(i, "N/A")
238
+ self._led_ring_effect.addItem(name, i)
239
+
240
+ self._led_ring_effect.currentIndexChanged.connect(self._ring_effect_changed)
241
+ self._led_ring_effect.setCurrentIndex(current)
242
+ self._led_ring_effect.setEnabled(int(self._helper.cf.param.values["deck"]["bcLedRing"]) == 1)
243
+ self._led_ring_headlight.setEnabled(int(self._helper.cf.param.values["deck"]["bcLedRing"]) == 1)
244
+
245
+ try:
246
+ self._helper.cf.param.set_value("ring.effect", "13")
247
+ self._led_ring_effect.setCurrentIndex(13)
248
+ self._ring_effect = 13
249
+ logger.info("Initialized LED ring to 'LED tab' mode (effect 13).")
250
+ except Exception as e:
251
+ logger.warning(f"Could not set LED tab effect on connect: {e}")
252
+
253
+ def _ring_effect_changed(self, index):
254
+ self._ring_effect = index
255
+ if index > -1:
256
+ i = self._led_ring_effect.itemData(index)
257
+ if i != int(self._helper.cf.param.values["ring"]["effect"]):
258
+ self._helper.cf.param.set_value("ring.effect", str(i))
259
+
260
+ if i == 13:
261
+ self._intensity_slider.setEnabled(True)
262
+ self._intensity_spin.setEnabled(True)
263
+ else:
264
+ self._intensity_slider.setEnabled(False)
265
+ self._intensity_spin.setEnabled(False)
266
+
267
+ def _ring_effect_updated(self, name, value):
268
+ if self._helper.cf.param.is_updated:
269
+ self._led_ring_effect.setCurrentIndex(int(value))
270
+
271
+ def alt1_updated(self, state):
272
+ if state:
273
+ new_index = (self._ring_effect+1) % (self._ledring_nbr_effects+1)
274
+ self._helper.cf.param.set_value("ring.effect", str(new_index))
275
+
276
+ def alt2_updated(self, state):
277
+ self._helper.cf.param.set_value("ring.headlightEnable", str(state))
@@ -99,7 +99,7 @@ class LogTab(TabToolbox, param_tab_class):
99
99
  log_groups = cfclient.log_param_doc['logs'][group]
100
100
  log_variable = log_groups['variables'][param]
101
101
  item.setData(4, Qt.ItemDataRole.DisplayRole, log_variable['short_desc'])
102
- except: # noqa
102
+ except (KeyError, TypeError, AttributeError):
103
103
  pass
104
104
 
105
105
  groupItem.addChild(item)
@@ -112,5 +112,5 @@ class LogTab(TabToolbox, param_tab_class):
112
112
  label = QtWidgets.QLabel(log_groups['desc'])
113
113
  label.setWordWrap(True)
114
114
  self.logTree.setItemWidget(groupItem, 4, label)
115
- except: # noqa
115
+ except (KeyError, TypeError, AttributeError):
116
116
  pass
@@ -78,6 +78,7 @@ class ParamChildItem(object):
78
78
  self._cf = crazyflie
79
79
  self.is_updating = True
80
80
  self.state = None
81
+ self.stored_value = ""
81
82
 
82
83
  def updated(self, name, value):
83
84
  """Callback from the param layer when a parameter has been updated"""
@@ -113,7 +114,7 @@ class ParamBlockModel(QAbstractItemModel):
113
114
  """Create the empty model"""
114
115
  super(ParamBlockModel, self).__init__(parent)
115
116
  self._nodes = []
116
- self._column_headers = ['Name', 'Type', 'Access', 'Persistent', 'Value']
117
+ self._column_headers = ['Name', 'Type', 'Access', 'Persistent', 'Value', 'Stored Value']
117
118
  self._red_brush = QBrush(QColor("red"))
118
119
  self._enabled = False
119
120
  self._mainUI = mainUI
@@ -227,12 +228,68 @@ class ParamBlockModel(QAbstractItemModel):
227
228
  return 'Yes' if node.persistent else 'No'
228
229
  if index.column() == 4:
229
230
  return node.value
231
+ if index.column() == 5:
232
+ return node.stored_value
230
233
  elif (role == Qt.ItemDataRole.BackgroundRole and index.column() == 4 and
231
234
  node.is_updating):
232
235
  return self._red_brush
233
236
 
234
237
  return None
235
238
 
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)
292
+
236
293
  def flags(self, index):
237
294
  """Re-implemented function for getting the flags for a certain index"""
238
295
  flag = super(ParamBlockModel, self).flags(index)
@@ -336,7 +393,17 @@ class ParamTab(TabToolbox, param_tab_class):
336
393
  def success_cb(name, success):
337
394
  print(f'store {success}!')
338
395
  if success:
339
- self.cf.param.persistent_get_state(name, lambda _, state: self._persistent_state_signal.emit(state))
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)
340
407
 
341
408
  complete = self.paramDetailsLabel.text()
342
409
  if self.persistentButton.text() == 'Clear':
@@ -398,7 +465,7 @@ class ParamTab(TabToolbox, param_tab_class):
398
465
 
399
466
  self.paramDetailsDescription.setWordWrap(True)
400
467
  self.paramDetailsDescription.setText(desc.replace('\n', ''))
401
- except: # noqa
468
+ except (KeyError, TypeError, AttributeError):
402
469
  self.paramDetailsDescription.setText('')
403
470
 
404
471
  complete = f'{group}.{param}'
@@ -417,6 +484,9 @@ class ParamTab(TabToolbox, param_tab_class):
417
484
 
418
485
  if elem.is_persistent():
419
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)
420
490
 
421
491
  def _update_param_io_buttons(self):
422
492
  enabled = self._is_connected
@@ -536,7 +606,7 @@ class ParamTab(TabToolbox, param_tab_class):
536
606
  def _clear_stored_persistent_params_button_clicked(self):
537
607
  dlg = QMessageBox(self)
538
608
  dlg.setWindowTitle("Clear Stored Parameters Confirmation")
539
- dlg.setText("Are you sure you want to clear your stored persistent parameter?")
609
+ dlg.setText("Are you sure you want to clear your stored persistent parameters?")
540
610
  dlg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
541
611
  button = dlg.exec()
542
612
 
@@ -544,6 +614,22 @@ class ParamTab(TabToolbox, param_tab_class):
544
614
  stored_persistent_params = self._get_all_stored_persistent_param_names()
545
615
  for complete_name in stored_persistent_params:
546
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
547
633
 
548
634
  def _connected(self, link_uri):
549
635
  self._model.reset()
@@ -34,7 +34,7 @@ from .CrtpSharkToolbox import CrtpSharkToolbox
34
34
  # from .ExampleTab import ExampleTab
35
35
  from .FlightTab import FlightTab
36
36
  # from .GpsTab import GpsTab
37
- from .LEDTab import LEDTab
37
+ from .LEDRingTab import LEDRingTab
38
38
  from .LogBlockTab import LogBlockTab
39
39
  from .LogTab import LogTab
40
40
  from .ParamTab import ParamTab
@@ -43,6 +43,7 @@ from .locopositioning_tab import LocoPositioningTab
43
43
  from .LogClientTab import LogClientTab
44
44
  from .lighthouse_tab import LighthouseTab
45
45
  from .TuningTab import TuningTab
46
+ from .ColorLEDTab import ColorLEDTab
46
47
 
47
48
  __author__ = 'Bitcraze AB'
48
49
  __all__ = []
@@ -52,7 +53,8 @@ available = [
52
53
  # ExampleTab,
53
54
  FlightTab,
54
55
  # GpsTab,
55
- LEDTab,
56
+ LEDRingTab,
57
+ ColorLEDTab,
56
58
  LogBlockTab,
57
59
  LogTab,
58
60
  ParamTab,