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
|
@@ -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
|
#
|
|
@@ -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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
92
|
-
self.clt.connectedSignal.connect(
|
|
93
|
-
|
|
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.
|
|
102
|
-
self.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
114
|
-
|
|
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
|
-
|
|
121
|
-
self.
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
156
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
178
|
-
msgBox.setText("Please choose an image file to program.")
|
|
424
|
+
self.clt.set_boot_mode(self.clt.WARM_BOOT)
|
|
179
425
|
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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.
|
|
195
|
-
self.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
531
|
+
def programAction(self, filename, mcu_to_flash):
|
|
266
532
|
targets = {}
|
|
267
|
-
if
|
|
268
|
-
targets[
|
|
533
|
+
if mcu_to_flash:
|
|
534
|
+
targets[mcu_to_flash] = ("fw",)
|
|
269
535
|
try:
|
|
270
|
-
self.
|
|
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')
|