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
  #
@@ -29,14 +29,27 @@
29
29
  The bootloader dialog is used to update the Crazyflie firmware and to
30
30
  read/write the configuration block in the Crazyflie flash.
31
31
  """
32
+ from __future__ import annotations
33
+
32
34
  from cflib.bootloader import Bootloader
35
+ from cfclient.ui.connectivity_manager import ConnectivityManager
33
36
 
37
+ import tempfile
34
38
  import logging
35
-
36
- from PyQt5 import QtWidgets, uic
37
- from PyQt5.QtCore import pyqtSlot, pyqtSignal, QThread
39
+ import json
40
+ import os
41
+ import re
42
+ import threading
43
+ from urllib.request import urlopen
44
+ from urllib.error import URLError
45
+ import zipfile
46
+
47
+ from PyQt6 import QtWidgets, uic
48
+ from PyQt6.QtCore import pyqtSlot, pyqtSignal, QThread, Qt
49
+ from PyQt6.QtGui import QPixmap
38
50
 
39
51
  import cfclient
52
+ import cflib.crazyflie
40
53
 
41
54
  __author__ = 'Bitcraze AB'
42
55
  __all__ = ['BootloaderDialog']
@@ -46,104 +59,209 @@ logger = logging.getLogger(__name__)
46
59
  service_dialog_class = uic.loadUiType(cfclient.module_path +
47
60
  "/ui/dialogs/bootloader.ui")[0]
48
61
 
62
+ # This url is used to fetch all the releases from the FirmwareDownloader
63
+ RELEASE_URL = 'https://api.github.com/repos/bitcraze/'\
64
+ 'crazyflie-release/releases'
49
65
 
50
- class UIState:
51
- DISCONNECTED = 0
52
- CONNECTING = 5
53
- CONNECT_FAILED = 1
54
- COLD_CONNECT = 2
55
- FLASHING = 3
56
- RESET = 4
66
+ ICON_PATH = os.path.join(cfclient.module_path, 'ui', 'icons')
57
67
 
58
68
 
59
69
  class BootloaderDialog(QtWidgets.QWidget, service_dialog_class):
60
70
  """Tab for update the Crazyflie firmware and for reading/writing the config
61
71
  block in flash"""
62
72
 
73
+ class UIState:
74
+ DISCONNECTED = 0
75
+ COLD_CONNECTING = 1
76
+ COLD_CONNECTED = 2
77
+ FW_CONNECTING = 3
78
+ FW_CONNECTED = 4
79
+ FW_SCANNING = 5
80
+ FLASHING = 6
81
+ RESET = 7
82
+
83
+ _release_firmwares_found = pyqtSignal(object)
84
+ _release_downloaded = pyqtSignal(str, object)
85
+
63
86
  def __init__(self, helper, *args):
64
87
  super(BootloaderDialog, self).__init__(*args)
65
88
  self.setupUi(self)
66
89
 
67
90
  self.tabName = "Service"
68
- self.menuName = "Service"
69
91
 
70
92
  # self.tabWidget = tabWidget
71
- self.helper = helper
93
+ self._helper = helper
72
94
 
73
95
  # self.cf = crazyflie
74
- self.clt = CrazyloadThread()
96
+ self.clt = CrazyloadThread(self._helper.cf)
75
97
 
76
98
  # Connecting GUI signals (a pity to do that manually...)
77
99
  self.imagePathBrowseButton.clicked.connect(self.pathBrowse)
78
100
  self.programButton.clicked.connect(self.programAction)
79
- self.verifyButton.clicked.connect(self.verifyAction)
80
101
  self.coldBootButton.clicked.connect(self.initiateColdboot)
81
102
  self.resetButton.clicked.connect(self.resetCopter)
82
- self._cancel_bootloading.clicked.connect(self.close)
103
+ self.sourceTab.currentChanged.connect(self._update_program_button_state)
104
+
105
+ self._helper.connectivity_manager.register_ui_elements(
106
+ ConnectivityManager.UiElementsContainer(
107
+ interface_combo=self.comboBox,
108
+ address_spinner=self.address,
109
+ connect_button=self.connectButton,
110
+ scan_button=self.scanButton))
111
+ self._helper.connectivity_manager.connection_state_changed.connect(self._fw_connection_state_changed)
83
112
 
84
113
  # connecting other signals
85
114
  self.clt.programmed.connect(self.programDone)
86
- self.clt.verified.connect(self.verifyDone)
87
115
  self.clt.statusChanged.connect(self.statusUpdate)
88
116
  # self.clt.updateBootloaderStatusSignal.connect(
89
117
  # self.updateBootloaderStatus)
90
- self.clt.connectingSignal.connect(
91
- lambda: self.setUiState(UIState.CONNECTING))
92
- self.clt.connectedSignal.connect(
93
- lambda: self.setUiState(UIState.COLD_CONNECT))
118
+ self.clt.connectingSignal.connect(lambda: self.setUiState(self.UIState.COLD_CONNECTING))
119
+
120
+ self.clt.connectedSignal.connect(lambda: self._load_thread_connection_event(self.UIState.COLD_CONNECTED))
121
+ self.clt.disconnectedSignal.connect(lambda: self._load_thread_connection_event(self.UIState.DISCONNECTED))
94
122
  self.clt.failed_signal.connect(lambda m: self._ui_connection_fail(m))
95
- self.clt.disconnectedSignal.connect(
96
- lambda: self.setUiState(UIState.DISCONNECTED))
97
123
 
124
+ self._cold_boot_error_message = None
125
+ self._state = self.UIState.DISCONNECTED
126
+
127
+ self._releases = {}
128
+ self._platform_widget_names = {}
129
+ self._release_firmwares_found.connect(self._populate_firmware_dropdown)
130
+ self._release_downloaded.connect(self.release_zip_downloaded)
131
+ self.firmware_downloader = FirmwareDownloader(self._release_firmwares_found, self._release_downloaded)
132
+ self.firmware_downloader.get_firmware_releases()
133
+
134
+ self.firmware_downloader.start()
98
135
  self.clt.start()
99
136
 
137
+ self._platform_filter_checkboxes = []
138
+
139
+ self._set_image(self.image_1, os.path.join(ICON_PATH, "bolt.webp"))
140
+ self._set_image(self.image_2, os.path.join(ICON_PATH, "cf21.webp"))
141
+ self._set_image(self.image_3, os.path.join(ICON_PATH, "bl.webp"))
142
+ self._set_image(self.image_4, os.path.join(ICON_PATH, "flapper.webp"))
143
+ self._set_image(self.image_5, os.path.join(ICON_PATH, "tag.webp"))
144
+
100
145
  def _ui_connection_fail(self, message):
101
- self.setStatusLabel(message)
102
- self.coldBootButton.setEnabled(True)
146
+ self._cold_boot_error_message = message
147
+ self.setUiState(self.UIState.DISCONNECTED)
148
+
149
+ def _set_image(self, image_label, image_path):
150
+ IMAGE_WIDTH = 100
151
+ IMAGE_HEIGHT = 100
152
+
153
+ pixmap = QPixmap(image_path)
154
+
155
+ if pixmap.isNull():
156
+ logger.warning(f"Failed to load image: {image_path}")
157
+ image_label.setText("Missing image")
158
+ else:
159
+ scaled_pixmap = pixmap.scaled(
160
+ IMAGE_WIDTH, IMAGE_HEIGHT,
161
+ Qt.AspectRatioMode.KeepAspectRatio,
162
+ Qt.TransformationMode.SmoothTransformation
163
+ )
164
+ image_label.setPixmap(scaled_pixmap)
103
165
 
104
166
  def setUiState(self, state):
105
- if (state == UIState.DISCONNECTED):
167
+ self._state = state
168
+
169
+ if (state == self.UIState.DISCONNECTED):
106
170
  self.resetButton.setEnabled(False)
107
171
  self.programButton.setEnabled(False)
108
- self.setStatusLabel("Not connected")
172
+ if self._cold_boot_error_message is not None:
173
+ self.setStatusLabel(self._cold_boot_error_message)
174
+ else:
175
+ self.setStatusLabel("Not connected")
109
176
  self.coldBootButton.setEnabled(True)
110
177
  self.progressBar.setTextVisible(False)
111
178
  self.progressBar.setValue(0)
112
179
  self.statusLabel.setText('Status: <b>IDLE</b>')
113
- self.imagePathLine.setText("")
114
- elif (state == UIState.CONNECTING):
180
+ self.setSourceSelectionUiEnabled(True)
181
+ self._helper.connectivity_manager.set_enable(True)
182
+ elif (state == self.UIState.COLD_CONNECTING):
183
+ self._cold_boot_error_message = None
115
184
  self.resetButton.setEnabled(False)
116
185
  self.programButton.setEnabled(False)
117
- self.setStatusLabel("Trying to connect cold bootloader, restart "
118
- "the Crazyflie to connect")
186
+ self.setStatusLabel("Trying to connect cold bootloader, restart the Crazyflie to connect")
119
187
  self.coldBootButton.setEnabled(False)
120
- elif (state == UIState.CONNECT_FAILED):
121
- self.setStatusLabel("Connecting to bootloader failed")
122
- self.coldBootButton.setEnabled(True)
123
- elif (state == UIState.COLD_CONNECT):
188
+ self.setSourceSelectionUiEnabled(True)
189
+ self._helper.connectivity_manager.set_enable(False)
190
+ elif (state == self.UIState.COLD_CONNECTED):
191
+ self._cold_boot_error_message = None
124
192
  self.resetButton.setEnabled(True)
125
- self.programButton.setEnabled(True)
193
+ if any(button.isChecked() for button in self._platform_filter_checkboxes):
194
+ self.programButton.setEnabled(True)
195
+ else:
196
+ self.programButton.setToolTip("Select a platform before programming.")
126
197
  self.setStatusLabel("Connected to bootloader")
127
198
  self.coldBootButton.setEnabled(False)
128
- elif (state == UIState.RESET):
199
+ self.imagePathBrowseButton.setEnabled(True)
200
+ self.imagePathLine.setEnabled(True)
201
+ self.firmwareDropdown.setEnabled(True)
202
+ self._helper.connectivity_manager.set_enable(False)
203
+ elif (state == self.UIState.FW_CONNECTING):
204
+ self._cold_boot_error_message = None
205
+ self.resetButton.setEnabled(False)
206
+ self.programButton.setEnabled(False)
207
+ self.setStatusLabel("Trying to connect in firmware mode")
208
+ self.coldBootButton.setEnabled(False)
209
+ self.setSourceSelectionUiEnabled(True)
210
+ self._helper.connectivity_manager.set_enable(True)
211
+ elif (state == self.UIState.FW_CONNECTED):
212
+ self._cold_boot_error_message = None
213
+ self.resetButton.setEnabled(False)
214
+ self.coldBootButton.setEnabled(False)
215
+ self._helper.connectivity_manager.set_enable(True)
216
+
217
+ if self._helper.cf.link_uri.startswith("usb://"):
218
+ self.programButton.setEnabled(False)
219
+ self.setStatusLabel("Connected using USB")
220
+ self.setSourceSelectionUiEnabled(False)
221
+ else:
222
+ if any(button.isChecked() for button in self._platform_filter_checkboxes):
223
+ self.programButton.setEnabled(True)
224
+ else:
225
+ self.programButton.setToolTip("Select a platform before programming.")
226
+ self.setStatusLabel("Connected in firmware mode")
227
+ self.setSourceSelectionUiEnabled(True)
228
+ elif (state == self.UIState.FW_SCANNING):
229
+ self._cold_boot_error_message = None
230
+ self.resetButton.setEnabled(False)
231
+ self.programButton.setEnabled(False)
232
+ self.setStatusLabel("Scanning")
233
+ self.coldBootButton.setEnabled(False)
234
+ self.setSourceSelectionUiEnabled(True)
235
+ self._helper.connectivity_manager.set_enable(True)
236
+ elif (state == self.UIState.FLASHING):
237
+ self.resetButton.setEnabled(False)
238
+ self.resetButton.setEnabled(False)
239
+ self.programButton.setEnabled(False)
240
+ self.setStatusLabel("Flashing")
241
+ self.coldBootButton.setEnabled(False)
242
+ self.setSourceSelectionUiEnabled(False)
243
+ self._helper.connectivity_manager.set_enable(False)
244
+ elif (state == self.UIState.RESET):
245
+ self._cold_boot_error_message = None
129
246
  self.setStatusLabel("Resetting to firmware, disconnected")
130
247
  self.resetButton.setEnabled(False)
131
248
  self.programButton.setEnabled(False)
132
249
  self.coldBootButton.setEnabled(False)
133
- self.imagePathLine.setText("")
250
+ self.setSourceSelectionUiEnabled(True)
251
+ self._helper.connectivity_manager.set_enable(False)
252
+ self._update_program_button_state()
253
+
254
+ def setSourceSelectionUiEnabled(self, enabled):
255
+ self.imagePathBrowseButton.setEnabled(enabled)
256
+ self.imagePathLine.setEnabled(enabled)
257
+ self.firmwareDropdown.setEnabled(enabled)
134
258
 
135
259
  def setStatusLabel(self, text):
136
260
  self.connectionStatus.setText("Status: <b>%s</b>" % text)
137
261
 
138
- def connected(self):
139
- self.setUiState(UIState.COLD_CONNECT)
140
-
141
- def connectionFailed(self):
142
- self.setUiState(UIState.CONNECT_FAILED)
143
-
144
262
  def resetCopter(self):
145
263
  self.clt.resetCopterSignal.emit()
146
- self.setUiState(UIState.RESET)
264
+ self.setUiState(self.UIState.RESET)
147
265
 
148
266
  def updateConfig(self, channel, speed, rollTrim, pitchTrim):
149
267
  self.rollTrim.setValue(rollTrim)
@@ -152,60 +270,198 @@ class BootloaderDialog(QtWidgets.QWidget, service_dialog_class):
152
270
  self.radioSpeed.setCurrentIndex(speed)
153
271
 
154
272
  def closeEvent(self, event):
155
- self.setUiState(UIState.RESET)
156
- self.clt.resetCopterSignal.emit()
273
+ self.clt.terminate_flashing()
274
+ # Remove downloaded-firmware files.
275
+ self.firmware_downloader.bootload_complete.emit()
276
+
277
+ def _populate_firmware_dropdown(self, releases):
278
+ """ Callback from firmware-downloader that retrieves all
279
+ the latest firmware-releases.
280
+ """
281
+ platforms = set()
282
+ for release in releases:
283
+ release_name = release[0]
284
+ downloads = release[1:]
285
+
286
+ downloads.sort(key=self.download_sorter)
287
+
288
+ for download in downloads:
289
+ download_name, download_link = download
290
+ platform = self._extract_platform(download_name)
291
+ # Ignore old releases that do not use the standard file naming convention
292
+ if platform:
293
+ widget_name = '%s - %s' % (release_name, download_name)
294
+ if platform not in self._platform_widget_names:
295
+ self._platform_widget_names[platform] = []
296
+ self._platform_widget_names[platform].append(widget_name)
297
+ self._releases[widget_name] = download_link
298
+
299
+ platforms.add(platform)
300
+
301
+ for platform in sorted(platforms, reverse=True):
302
+ RADIO_BUTTON_WIDTH = 100
303
+
304
+ radio_button = QtWidgets.QRadioButton(platform)
305
+
306
+ radio_button.setFixedWidth(RADIO_BUTTON_WIDTH)
307
+
308
+ radio_button.toggled.connect(self._update_firmware_dropdown)
309
+ radio_button.toggled.connect(self._update_program_button_state)
310
+
311
+ self._platform_filter_checkboxes.append(radio_button)
312
+ self.filterLayout.insertWidget(0, radio_button)
313
+
314
+ self.firmwareDropdown.setSizeAdjustPolicy(QtWidgets.QComboBox.SizeAdjustPolicy.AdjustToContents)
315
+ self._update_firmware_dropdown(True)
316
+
317
+ def _has_selected_file(self) -> bool:
318
+ return bool(self.imagePathLine.text())
319
+
320
+ def _update_program_button_state(self):
321
+ is_connected = self._state in (
322
+ self.UIState.COLD_CONNECTED,
323
+ self.UIState.FW_CONNECTED
324
+ )
325
+
326
+ if not is_connected:
327
+ self.programButton.setEnabled(False)
328
+ self.programButton.setToolTip("Connect your device before programming.")
329
+ return
330
+
331
+ current_tab = self.sourceTab.currentWidget()
332
+
333
+ if current_tab == self.tabFromFile:
334
+ has_file = bool(self.imagePathLine.text())
335
+ self.programButton.setEnabled(has_file)
336
+
337
+ self.programButton.setToolTip(
338
+ "" if has_file else "Choose a firmware file to program."
339
+ )
340
+ else:
341
+ any_platform_checked = any(
342
+ button.isChecked() for button in self._platform_filter_checkboxes
343
+ )
344
+
345
+ self.programButton.setEnabled(any_platform_checked)
346
+ self.programButton.setToolTip(
347
+ "" if any_platform_checked else "Select a platform before programming."
348
+ )
349
+
350
+ def _update_firmware_dropdown(self, active: bool):
351
+ if active:
352
+ platform = None
353
+ for button in self._platform_filter_checkboxes:
354
+ if button.isChecked():
355
+ platform = button.text()
356
+
357
+ if platform:
358
+ self.firmwareDropdown.clear()
359
+ for widget_name in self._platform_widget_names[platform]:
360
+ self.firmwareDropdown.addItem(widget_name)
361
+
362
+ def _extract_platform(self, download_name: str) -> str | None:
363
+ # Download name is something like 'firmware-cf2-2022.12.zip'
364
+ found = re.search('firmware-(\\w+)-', download_name)
365
+ if found:
366
+ groups = found.groups()
367
+ if len(groups) == 1:
368
+ return groups[0]
369
+ return None
370
+
371
+ def download_sorter(self, element):
372
+ '''Sort downloads to display cf2 before bolt and tag'''
373
+ name = element[0]
374
+ if 'cf2' in name:
375
+ return '0' + name
376
+ else:
377
+ return '1' + name
378
+
379
+ def release_zip_downloaded(self, release_name, release_path):
380
+ """ Callback when a release is successfully downloaded and
381
+ save to release_path.
382
+ """
383
+ self.downloadStatus.setText('Downloaded')
384
+ self.clt.program.emit(release_path, '')
385
+
386
+ def _load_thread_connection_event(self, new_sate):
387
+ if self._state != self.UIState.FLASHING:
388
+ self.setUiState(new_sate)
389
+
390
+ def _fw_connection_state_changed(self, new_state):
391
+ if self._state != self.UIState.FLASHING:
392
+ if new_state == ConnectivityManager.UIState.DISCONNECTED:
393
+ self.setUiState(self.UIState.DISCONNECTED)
394
+ elif new_state == ConnectivityManager.UIState.CONNECTING:
395
+ self.setUiState(self.UIState.FW_CONNECTING)
396
+ elif new_state == ConnectivityManager.UIState.CONNECTED:
397
+ self.setUiState(self.UIState.FW_CONNECTED)
398
+ elif new_state == ConnectivityManager.UIState.SCANNING:
399
+ self.setUiState(self.UIState.FW_SCANNING)
157
400
 
158
401
  @pyqtSlot()
159
402
  def pathBrowse(self):
160
- filename = ""
161
- # Fix for crash in X on Ubuntu 14.04
162
- filename, _ = QtWidgets.QFileDialog.getOpenFileName()
163
- if filename != "":
403
+ names = QtWidgets.QFileDialog.getOpenFileName(
404
+ self, 'Release file to flash', self._helper.current_folder, "*.zip")
405
+ if names[0] == '':
406
+ return
407
+
408
+ filename = names[0]
409
+ self._helper.current_folder = os.path.dirname(filename)
410
+
411
+ if filename.endswith('.zip'):
164
412
  self.imagePathLine.setText(filename)
165
- pass
413
+ self._update_program_button_state()
414
+ else:
415
+ msgBox = QtWidgets.QMessageBox()
416
+ msgBox.setText("Wrong file extention. Must be .zip.")
417
+ msgBox.exec_()
166
418
 
167
419
  @pyqtSlot()
168
420
  def programAction(self):
169
- # self.setStatusLabel("Initiate programming")
170
- self.resetButton.setEnabled(False)
171
- self.programButton.setEnabled(False)
172
- self.imagePathBrowseButton.setEnabled(False)
173
- if self.imagePathLine.text() != "":
174
- self.clt.program.emit(self.imagePathLine.text(),
175
- self.verifyCheckBox.isChecked())
421
+ if self._state == self.UIState.COLD_CONNECTED:
422
+ self.clt.set_boot_mode(self.clt.COLD_BOOT)
176
423
  else:
177
- msgBox = QtWidgets.QMessageBox()
178
- msgBox.setText("Please choose an image file to program.")
424
+ self.clt.set_boot_mode(self.clt.WARM_BOOT)
179
425
 
180
- msgBox.exec_()
426
+ # call the flasher
427
+ if self.sourceTab.currentWidget() == self.tabFromFile:
428
+ if self.imagePathLine.text() == "":
429
+ msgBox = QtWidgets.QMessageBox()
430
+ msgBox.setText("Please choose an image file to program.")
431
+ msgBox.exec_()
181
432
 
182
- @pyqtSlot()
183
- def verifyAction(self):
184
- self.statusLabel.setText('Status: <b>Initiate verification</b>')
185
- pass
433
+ return
434
+
435
+ self.setUiState(self.UIState.FLASHING)
436
+
437
+ # by default, flash everything in the zip (if possible)
438
+ mcu_to_flash = None
439
+ self.clt.program.emit(self.imagePathLine.text(), mcu_to_flash)
440
+ else:
441
+ self.setUiState(self.UIState.FLASHING)
442
+
443
+ requested_release = self.firmwareDropdown.currentText()
444
+ download_url = self._releases[requested_release]
445
+ self.downloadStatus.setText('Fetching...')
446
+ self.firmware_downloader.download_release(requested_release, download_url)
186
447
 
187
448
  @pyqtSlot(bool)
188
449
  def programDone(self, success):
189
450
  if success:
190
451
  self.statusLabel.setText('Status: <b>Programing complete!</b>')
452
+ self.downloadStatus.setText('')
191
453
  else:
192
454
  self.statusLabel.setText('Status: <b>Programing failed!</b>')
193
455
 
194
- self.resetButton.setEnabled(True)
195
- self.programButton.setEnabled(True)
196
- self.imagePathBrowseButton.setEnabled(True)
197
-
198
- @pyqtSlot()
199
- def verifyDone(self):
200
- self.statusLabel.setText('Status: <b>Verification complete</b>')
201
- pass
456
+ self.setUiState(self.UIState.DISCONNECTED)
457
+ self.resetCopter()
202
458
 
203
459
  @pyqtSlot(str, int)
204
460
  def statusUpdate(self, status, progress):
205
461
  logger.debug("Status: [%s] | %d", status, progress)
206
462
  self.statusLabel.setText('Status: <b>' + status + '</b>')
207
463
  if progress >= 0:
208
- self.progressBar.setValue(progress)
464
+ self.progressBar.setValue(int(progress))
209
465
 
210
466
  def initiateColdboot(self):
211
467
  self.clt.initiateColdBootSignal.emit("radio://0/100")
@@ -215,8 +471,7 @@ class BootloaderDialog(QtWidgets.QWidget, service_dialog_class):
215
471
  # event loop which is what we want
216
472
  class CrazyloadThread(QThread):
217
473
  # Input signals declaration (not sure it should be used like that...)
218
- program = pyqtSignal(str, bool)
219
- verify = pyqtSignal()
474
+ program = pyqtSignal(str, str)
220
475
  initiateColdBootSignal = pyqtSignal(str)
221
476
  resetCopterSignal = pyqtSignal()
222
477
  writeConfigSignal = pyqtSignal(int, int, float, float)
@@ -233,11 +488,19 @@ class CrazyloadThread(QThread):
233
488
 
234
489
  radioSpeedPos = 2
235
490
 
236
- def __init__(self):
491
+ WARM_BOOT = 0
492
+ COLD_BOOT = 1
493
+
494
+ def __init__(self, crazyflie: cflib.crazyflie.Crazyflie):
237
495
  super(CrazyloadThread, self).__init__()
238
496
 
497
+ self._terminate_flashing = False
498
+
239
499
  self._bl = Bootloader()
240
- self._bl.progress_cb = self.statusChanged.emit
500
+
501
+ self._cf = crazyflie
502
+
503
+ self._boot_mode = self.COLD_BOOT
241
504
 
242
505
  # Make sure that the signals are handled by this thread event loop
243
506
  self.moveToThread(self)
@@ -250,11 +513,14 @@ class CrazyloadThread(QThread):
250
513
  self.quit()
251
514
  self.wait()
252
515
 
516
+ def set_boot_mode(self, mode):
517
+ self._boot_mode = mode
518
+
253
519
  def initiateColdBoot(self, linkURI):
254
520
  self.connectingSignal.emit()
255
521
 
256
522
  try:
257
- success = self._bl.start_bootloader(warm_boot=False)
523
+ success = self._bl.start_bootloader(warm_boot=False, cf=None)
258
524
  if not success:
259
525
  self.failed_signal.emit("Could not connect to bootloader")
260
526
  else:
@@ -262,16 +528,28 @@ class CrazyloadThread(QThread):
262
528
  except Exception as e:
263
529
  self.failed_signal.emit("{}".format(e))
264
530
 
265
- def programAction(self, filename, verify):
531
+ def programAction(self, filename, mcu_to_flash):
266
532
  targets = {}
267
- if str(filename).endswith("bin"):
268
- targets["stm32"] = ("fw",)
533
+ if mcu_to_flash:
534
+ targets[mcu_to_flash] = ("fw",)
269
535
  try:
270
- self._bl.flash(str(filename), targets)
536
+ self._terminate_flashing = False
537
+ self._bl.clink = self._cf.link_uri
538
+ self._bl.flash_full(self._cf,
539
+ str(filename),
540
+ self._boot_mode is self.WARM_BOOT,
541
+ targets,
542
+ None,
543
+ self.statusChanged.emit,
544
+ lambda: self._terminate_flashing)
271
545
  self.programmed.emit(True)
272
- except Exception:
546
+ except Exception as e:
547
+ self.failed_signal.emit("{}".format(e))
273
548
  self.programmed.emit(False)
274
549
 
550
+ def terminate_flashing(self):
551
+ self._terminate_flashing = True
552
+
275
553
  def resetCopter(self):
276
554
  try:
277
555
  self._bl.reset_to_firmware()
@@ -279,3 +557,88 @@ class CrazyloadThread(QThread):
279
557
  pass
280
558
  self._bl.close()
281
559
  self.disconnectedSignal.emit()
560
+
561
+
562
+ class FirmwareDownloader(QThread):
563
+ """ Uses github API to retrieves firmware-releases. """
564
+
565
+ bootload_complete = pyqtSignal()
566
+
567
+ def __init__(self, qtsignal_get_all_firmwares, qtsignal_get_release):
568
+ super(FirmwareDownloader, self).__init__()
569
+
570
+ self._qtsignal_get_all_firmwares = qtsignal_get_all_firmwares
571
+ self._qtsignal_get_release = qtsignal_get_release
572
+
573
+ self._tempDirectory = tempfile.TemporaryDirectory()
574
+
575
+ self.moveToThread(self)
576
+
577
+ def get_firmware_releases(self):
578
+ """ Wrapper-function """
579
+ threading.Thread(target=self._get_firmware_releases,
580
+ args=(self._qtsignal_get_all_firmwares, )).start()
581
+
582
+ def download_release(self, release_name, url):
583
+ """ Wrapper-function """
584
+ threading.Thread(target=self._download_release,
585
+ args=(self._qtsignal_get_release,
586
+ release_name, url)).start()
587
+
588
+ def _get_firmware_releases(self, signal):
589
+ """ Gets the firmware releases from the github API
590
+ and returns a list of format [rel-name, {release: download-link}].
591
+ Returns None if the request fails.
592
+ """
593
+ response = {}
594
+ try:
595
+ with urlopen(RELEASE_URL) as resp:
596
+ response = json.load(resp)
597
+ except URLError:
598
+ logger.warning(
599
+ 'Failed to make web request to get firmware-release')
600
+
601
+ release_list = []
602
+
603
+ for release in response:
604
+ release_name = release['name']
605
+ if release_name:
606
+ releases = [release_name]
607
+ for download in release['assets']:
608
+ releases.append((download['name'], download['browser_download_url']))
609
+ release_list.append(releases)
610
+
611
+ if release_list:
612
+ signal.emit(release_list)
613
+ else:
614
+ logger.warning('Failed to parse firmware-releases in web request')
615
+
616
+ def _download_release(self, signal, release_name, url):
617
+ """ Downloads the given release and calls the callback signal
618
+ if successful.
619
+ """
620
+ filepath = os.path.join(self._tempDirectory.name, release_name.split(' ')[-1])
621
+ try:
622
+ # Check if we have an old file saved and if so, ensure it's a valid
623
+ # zipfile and then call signal
624
+ with open(filepath, 'rb') as f:
625
+ previous_release = zipfile.ZipFile(f)
626
+ # testzip returns None if it's OK.
627
+ if previous_release.testzip() is None:
628
+ logger.info('Using same firmware-release file at'
629
+ '%s' % filepath)
630
+ signal.emit(release_name, filepath)
631
+ return
632
+ except FileNotFoundError:
633
+ try:
634
+ # Fetch the file with a new web request and save it to
635
+ # a temporary file.
636
+ with urlopen(url) as response:
637
+ with open(filepath, 'wb') as release_file:
638
+ release_file.write(response.read())
639
+ logger.info('Created temporary firmware-release file at'
640
+ '%s' % filepath)
641
+ signal.emit(release_name, filepath)
642
+ except URLError:
643
+ logger.warning('Failed to make web request to get requested'
644
+ ' firmware-release')