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
cfclient/ui/main.py CHANGED
@@ -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
  #
@@ -28,61 +28,58 @@ The main file for the Crazyflie control application.
28
28
  """
29
29
  import logging
30
30
  import sys
31
+ import usb
31
32
 
32
33
  import cfclient
34
+ from cfclient.ui.pose_logger import PoseLogger
35
+ from cfclient.ui.tab_toolbox import TabToolbox
33
36
  import cfclient.ui.tabs
34
- import cfclient.ui.toolboxes
35
37
  import cflib.crtp
36
38
  from cfclient.ui.dialogs.about import AboutDialog
37
39
  from cfclient.ui.dialogs.bootloader import BootloaderDialog
40
+ from cfclient.ui.connectivity_manager import ConnectivityManager
38
41
  from cfclient.utils.config import Config
39
42
  from cfclient.utils.config_manager import ConfigManager
40
43
  from cfclient.utils.input import JoystickReader
41
44
  from cfclient.utils.logconfigreader import LogConfigReader
45
+ from cfclient.utils.ui import UiUtils
42
46
  from cfclient.utils.zmq_led_driver import ZMQLEDDriver
43
47
  from cfclient.utils.zmq_param import ZMQParamAccess
44
48
  from cflib.crazyflie import Crazyflie
45
49
  from cflib.crazyflie.log import LogConfig
46
50
  from cflib.crazyflie.mem import MemoryElement
47
- from PyQt5 import QtWidgets
48
- from PyQt5 import uic
49
- from PyQt5.QtCore import pyqtSignal
50
- from PyQt5.QtCore import pyqtSlot
51
- from PyQt5.QtCore import QDir
52
- from PyQt5.QtCore import QThread
53
- from PyQt5.QtCore import QUrl
54
- from PyQt5.QtWidgets import QAction
55
- from PyQt5.QtWidgets import QActionGroup
56
- from PyQt5.QtGui import QDesktopServices
57
- from PyQt5.QtWidgets import QLabel
58
- from PyQt5.QtWidgets import QMenu
59
- from PyQt5.QtWidgets import QMessageBox
60
-
61
- from .dialogs.cf1config import Cf1ConfigDialog
51
+ from PyQt6 import QtWidgets
52
+ from PyQt6 import uic
53
+ from PyQt6.QtCore import pyqtSignal
54
+ from PyQt6.QtCore import pyqtSlot
55
+ from PyQt6.QtCore import QDir
56
+ from PyQt6.QtCore import QThread
57
+ from PyQt6.QtCore import QUrl
58
+ from PyQt6.QtCore import Qt
59
+ from PyQt6.QtGui import QAction
60
+ from PyQt6.QtGui import QActionGroup
61
+ from PyQt6.QtGui import QShortcut
62
+ from PyQt6.QtGui import QDesktopServices
63
+ from PyQt6.QtGui import QPalette
64
+ from PyQt6.QtWidgets import QLabel
65
+ from PyQt6.QtWidgets import QMenu
66
+ from PyQt6.QtWidgets import QMessageBox
67
+
62
68
  from .dialogs.cf2config import Cf2ConfigDialog
63
69
  from .dialogs.inputconfigdialogue import InputConfigDialogue
64
70
  from .dialogs.logconfigdialogue import LogConfigDialogue
65
71
 
72
+
66
73
  __author__ = 'Bitcraze AB'
67
74
  __all__ = ['MainUI']
68
75
 
69
76
  logger = logging.getLogger(__name__)
70
77
 
71
- INTERFACE_PROMPT_TEXT = 'Select an interface'
72
-
73
78
  (main_window_class,
74
79
  main_windows_base_class) = (uic.loadUiType(cfclient.module_path +
75
80
  '/ui/main.ui'))
76
81
 
77
82
 
78
- class MyDockWidget(QtWidgets.QDockWidget):
79
- closed = pyqtSignal()
80
-
81
- def closeEvent(self, event):
82
- super(MyDockWidget, self).closeEvent(event)
83
- self.closed.emit()
84
-
85
-
86
83
  class UIState:
87
84
  DISCONNECTED = 0
88
85
  CONNECTING = 1
@@ -94,24 +91,6 @@ class BatteryStates:
94
91
  BATTERY, CHARGING, CHARGED, LOW_POWER = list(range(4))
95
92
 
96
93
 
97
- COLOR_BLUE = '#3399ff'
98
- COLOR_GREEN = '#00ff60'
99
- COLOR_RED = '#cc0404'
100
-
101
-
102
- def progressbar_stylesheet(color):
103
- return """
104
- QProgressBar {
105
- border: 1px solid #333;
106
- background-color: transparent;
107
- }
108
-
109
- QProgressBar::chunk {
110
- background-color: """ + color + """;
111
- }
112
- """
113
-
114
-
115
94
  class MainUI(QtWidgets.QMainWindow, main_window_class):
116
95
  connectionLostSignal = pyqtSignal(str, str)
117
96
  connectionInitiatedSignal = pyqtSignal(str)
@@ -119,7 +98,7 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
119
98
  connectionDoneSignal = pyqtSignal(str)
120
99
  connectionFailedSignal = pyqtSignal(str, str)
121
100
  disconnectedSignal = pyqtSignal(str)
122
- linkQualitySignal = pyqtSignal(int)
101
+ linkQualitySignal = pyqtSignal(float)
123
102
 
124
103
  _input_device_error_signal = pyqtSignal(str)
125
104
  _input_discovery_signal = pyqtSignal(object)
@@ -136,47 +115,10 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
136
115
  except KeyError:
137
116
  pass
138
117
 
139
- ######################################################
140
- # By lxrocks
141
- # 'Skinny Progress Bar' tweak for Yosemite
142
- # Tweak progress bar - artistic I am not - so pick your own colors !!!
143
- # Only apply to Yosemite
144
- ######################################################
145
- import platform
146
-
147
- if platform.system() == 'Darwin':
148
-
149
- (Version, junk, machine) = platform.mac_ver()
150
- logger.info("This is a MAC - checking if we can apply Progress "
151
- "Bar Stylesheet for Yosemite Skinny Bars ")
152
- yosemite = (10, 10, 0)
153
- tVersion = tuple(map(int, (Version.split("."))))
154
-
155
- if tVersion >= yosemite:
156
- logger.info("Found Yosemite - applying stylesheet")
157
-
158
- tcss = """
159
- QProgressBar {
160
- border: 1px solid grey;
161
- border-radius: 5px;
162
- text-align: center;
163
- }
164
- QProgressBar::chunk {
165
- background-color: """ + COLOR_BLUE + """;
166
- }
167
- """
168
- self.setStyleSheet(tcss)
169
-
170
- else:
171
- logger.info("Pre-Yosemite - skinny bar stylesheet not applied")
172
-
173
- ######################################################
174
-
175
118
  self.cf = Crazyflie(ro_cache=None,
176
119
  rw_cache=cfclient.config_path + "/cache")
177
120
 
178
- cflib.crtp.init_drivers(enable_debug_driver=Config()
179
- .get("enable_debug_driver"))
121
+ cflib.crtp.init_drivers()
180
122
 
181
123
  zmq_params = ZMQParamAccess(self.cf)
182
124
  zmq_params.start()
@@ -193,15 +135,26 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
193
135
  " fly.")
194
136
  self.statusBar().addWidget(self._statusbar_label)
195
137
 
138
+ #
139
+ # We use this hacky-trick to find out if we are in dark-mode and
140
+ # figure out what bgcolor to set from that. We always use the current
141
+ # palette forgreound.
142
+ #
143
+ self.textColor = self._statusbar_label.palette().color(QPalette.ColorRole.WindowText)
144
+ self.bgColor = self._statusbar_label.palette().color(QPalette.ColorRole.Window)
145
+ self.isDark = self.textColor.value() > self.bgColor.value()
146
+
196
147
  self.joystickReader = JoystickReader()
197
148
  self._active_device = ""
198
149
  # self.configGroup = QActionGroup(self._menu_mappings, exclusive=True)
199
150
 
200
- self._mux_group = QActionGroup(self._menu_inputdevice, exclusive=True)
151
+ self._mux_group = QActionGroup(self._menu_inputdevice)
152
+ self._mux_group.setExclusive(True)
201
153
 
202
154
  # TODO: Need to reload configs
203
155
  # ConfigManager().conf_needs_reload.add_callback(self._reload_configs)
204
156
 
157
+ self.connect_input = QShortcut("Ctrl+I", self.connectButton, self._connect)
205
158
  self.cf.connection_failed.add_callback(
206
159
  self.connectionFailedSignal.emit)
207
160
  self.connectionFailedSignal.connect(self._connection_failed)
@@ -214,17 +167,8 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
214
167
  self.joystickReader.device_discovery.add_callback(
215
168
  self._input_discovery_signal.emit)
216
169
 
217
- # Hide the 'File' menu on OS X, since its only item, 'Exit', gets
218
- # merged into the application menu.
219
- if sys.platform == 'darwin':
220
- self.menuFile.menuAction().setVisible(False)
221
-
222
170
  # Connect UI signals
223
171
  self.logConfigAction.triggered.connect(self._show_connect_dialog)
224
- self.interfaceCombo.currentIndexChanged['QString'].connect(
225
- self.interfaceChanged)
226
- self.connectButton.clicked.connect(self._connect)
227
- self.scanButton.clicked.connect(self._scan)
228
172
  self.menuItemConnect.triggered.connect(self._connect)
229
173
  self.menuItemConfInputDevice.triggered.connect(
230
174
  self._show_input_device_config_dialog)
@@ -234,21 +178,38 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
234
178
  self._menuItem_openconfigfolder.triggered.connect(
235
179
  self._open_config_folder)
236
180
 
237
- self.address.setValue(0xE7E7E7E7E7)
181
+ self._set_address()
182
+
183
+ self._connectivity_manager = ConnectivityManager()
184
+ self._connectivity_manager.register_ui_elements(
185
+ ConnectivityManager.UiElementsContainer(
186
+ interface_combo=self.interfaceCombo,
187
+ address_spinner=self.address,
188
+ connect_button=self.connectButton,
189
+ scan_button=self.scanButton))
190
+
191
+ self._connectivity_manager.connect_button_clicked.connect(self._connect)
192
+ self._connectivity_manager.scan_button_clicked.connect(self._scan_from_button)
238
193
 
239
- self._auto_reconnect_enabled = Config().get("auto_reconnect")
240
- self.autoReconnectCheckBox.toggled.connect(
241
- self._auto_reconnect_changed)
242
- self.autoReconnectCheckBox.setChecked(Config().get("auto_reconnect"))
194
+ self._disable_input = False
243
195
 
244
196
  self.joystickReader.input_updated.add_callback(
245
- self.cf.commander.send_setpoint)
197
+ lambda *args: self._disable_input or
198
+ self.cf.commander.send_setpoint(*args))
246
199
 
247
200
  self.joystickReader.assisted_input_updated.add_callback(
248
- self.cf.commander.send_velocity_world_setpoint)
201
+ lambda *args: self._disable_input or
202
+ self.cf.commander.send_velocity_world_setpoint(*args))
249
203
 
250
204
  self.joystickReader.heighthold_input_updated.add_callback(
251
- self.cf.commander.send_zdistance_setpoint)
205
+ lambda *args: self._disable_input or
206
+ self.cf.commander.send_zdistance_setpoint(*args))
207
+
208
+ self.joystickReader.hover_input_updated.add_callback(
209
+ self.cf.commander.send_hover_setpoint)
210
+
211
+ # Emergency stop button
212
+ self.esButton.clicked.connect(self._emergency_stop)
252
213
 
253
214
  # Connection callbacks and signal wrappers for UI protection
254
215
  self.cf.connected.add_callback(self.connectionDoneSignal.emit)
@@ -263,19 +224,12 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
263
224
  self._log_error_signal.connect(self._logging_error)
264
225
 
265
226
  self.batteryBar.setTextVisible(False)
266
- self.batteryBar.setStyleSheet(progressbar_stylesheet(COLOR_BLUE))
267
-
268
227
  self.linkQualityBar.setTextVisible(False)
269
- self.linkQualityBar.setStyleSheet(progressbar_stylesheet(COLOR_BLUE))
270
228
 
271
229
  # Connect link quality feedback
272
- self.cf.link_quality_updated.add_callback(self.linkQualitySignal.emit)
230
+ self.cf.link_statistics.link_quality_updated.add_callback(self.linkQualitySignal.emit)
273
231
  self.linkQualitySignal.connect(
274
- lambda percentage: self.linkQualityBar.setValue(percentage))
275
-
276
- self._selected_interface = None
277
- self._initial_scan = True
278
- self._scan()
232
+ lambda percentage: self.linkQualityBar.setValue(int(percentage)))
279
233
 
280
234
  # Parse the log configuration files
281
235
  self.logConfigReader = LogConfigReader(self.cf)
@@ -290,69 +244,33 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
290
244
  cfclient.ui.pluginhelper.cf = self.cf
291
245
  cfclient.ui.pluginhelper.inputDeviceReader = self.joystickReader
292
246
  cfclient.ui.pluginhelper.logConfigReader = self.logConfigReader
247
+ cfclient.ui.pluginhelper.pose_logger = PoseLogger(self.cf)
248
+ cfclient.ui.pluginhelper.connectivity_manager = self._connectivity_manager
249
+ cfclient.ui.pluginhelper.mainUI = self
293
250
 
294
251
  self.logConfigDialogue = LogConfigDialogue(cfclient.ui.pluginhelper)
295
252
  self._bootloader_dialog = BootloaderDialog(cfclient.ui.pluginhelper)
296
253
  self._cf2config_dialog = Cf2ConfigDialog(cfclient.ui.pluginhelper)
297
- self._cf1config_dialog = Cf1ConfigDialog(cfclient.ui.pluginhelper)
298
254
  self.menuItemBootloader.triggered.connect(self._bootloader_dialog.show)
299
255
  self._about_dialog = AboutDialog(cfclient.ui.pluginhelper)
300
256
  self.menuItemAbout.triggered.connect(self._about_dialog.show)
301
257
  self._menu_cf2_config.triggered.connect(self._cf2config_dialog.show)
302
- self._menu_cf1_config.triggered.connect(self._cf1config_dialog.show)
303
-
304
- # Load and connect tabs
305
- self.tabsMenuItem = QMenu("Tabs", self.menuView, enabled=True)
306
- self.menuView.addMenu(self.tabsMenuItem)
307
-
308
- # self.tabsMenuItem.setMenu(QtWidgets.QMenu())
309
- tabItems = {}
310
- self.loadedTabs = []
311
- for tabClass in cfclient.ui.tabs.available:
312
- tab = tabClass(self.tabs, cfclient.ui.pluginhelper)
313
- item = QtWidgets.QAction(tab.getMenuName(), self, checkable=True)
314
- item.toggled.connect(tab.toggleVisibility)
315
- self.tabsMenuItem.addAction(item)
316
- tabItems[tab.getTabName()] = item
317
- self.loadedTabs.append(tab)
318
- if not tab.enabled:
319
- item.setEnabled(False)
320
-
321
- # First instantiate all tabs and then open them in the correct order
322
- try:
323
- for tName in Config().get("open_tabs").split(","):
324
- t = tabItems[tName]
325
- if (t is not None and t.isEnabled()):
326
- # Toggle though menu so it's also marked as open there
327
- t.toggle()
328
- except Exception as e:
329
- logger.warning("Exception while opening tabs [{}]".format(e))
330
-
331
- # Loading toolboxes (A bit of magic for a lot of automatic)
332
- self.toolboxesMenuItem = QMenu("Toolboxes", self.menuView,
333
- enabled=True)
334
- self.menuView.addMenu(self.toolboxesMenuItem)
335
258
 
336
- self.toolboxes = []
337
- for t_class in cfclient.ui.toolboxes.toolboxes:
338
- toolbox = t_class(cfclient.ui.pluginhelper)
339
- dockToolbox = MyDockWidget(toolbox.getName())
340
- dockToolbox.setWidget(toolbox)
341
- self.toolboxes += [dockToolbox, ]
259
+ self._connectivity_manager.set_address(self.address.value())
260
+
261
+ self._initial_scan = True
262
+ self._scan(self._connectivity_manager.get_address())
342
263
 
343
- # Add menu item for the toolbox
344
- item = QtWidgets.QAction(toolbox.getName(), self)
345
- item.setCheckable(True)
346
- item.triggered.connect(self.toggleToolbox)
347
- self.toolboxesMenuItem.addAction(item)
264
+ self.tabs_menu_item = QMenu("Tabs", self.menuView, enabled=True)
265
+ self.menuView.addMenu(self.tabs_menu_item)
348
266
 
349
- dockToolbox.closed.connect(lambda: self.toggleToolbox(False))
267
+ self.toolboxes_menu_item = QMenu("Toolboxes", self.menuView, enabled=True)
268
+ self.menuView.addMenu(self.toolboxes_menu_item)
350
269
 
351
- # Setup some introspection
352
- item.dockToolbox = dockToolbox
353
- item.menuItem = item
354
- dockToolbox.dockToolbox = dockToolbox
355
- dockToolbox.menuItem = item
270
+ self.loaded_tab_toolboxes = self.create_tab_toolboxes(self.tabs_menu_item,
271
+ self.toolboxes_menu_item,
272
+ self.tab_widget)
273
+ self.read_tab_toolbox_config(self.loaded_tab_toolboxes)
356
274
 
357
275
  # References to all the device sub-menus in the "Input device" menu
358
276
  self._all_role_menus = ()
@@ -363,7 +281,8 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
363
281
  self._all_mux_nodes = ()
364
282
 
365
283
  # Check which Input muxes are available
366
- self._mux_group = QActionGroup(self._menu_inputdevice, exclusive=True)
284
+ self._mux_group = QActionGroup(self._menu_inputdevice)
285
+ self._mux_group.setExclusive(True)
367
286
  for m in self.joystickReader.available_mux():
368
287
  node = QAction(m.name,
369
288
  self._menu_inputdevice,
@@ -386,18 +305,110 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
386
305
 
387
306
  self._mapping_support = True
388
307
 
389
- def interfaceChanged(self, interface):
390
- if interface == INTERFACE_PROMPT_TEXT:
391
- self._selected_interface = None
392
- else:
393
- self._selected_interface = interface
394
- self._update_ui_state()
308
+ # Add checkbuttons for theme-selection.
309
+ self._theme_group = QActionGroup(self.menuThemes)
310
+ self._theme_group.setExclusive(True)
311
+ self._theme_checkboxes = []
312
+ for theme in UiUtils.THEMES:
313
+ node = QAction(theme, self.menuThemes, checkable=True)
314
+ node.setObjectName(theme)
315
+ node.toggled.connect(self._theme_selected)
316
+ self._theme_checkboxes.append(node)
317
+ self._theme_group.addAction(node)
318
+ self.menuThemes.addAction(node)
319
+
320
+ # We only want to warn about USB permission once
321
+ self._permission_warned = False
322
+
323
+ def create_tab_toolboxes(self, tabs_menu_item, toolboxes_menu_item, tab_widget):
324
+ loaded_tab_toolboxes = {}
325
+
326
+ for tab_class in cfclient.ui.tabs.available:
327
+ tab_toolbox = tab_class(cfclient.ui.pluginhelper)
328
+ loaded_tab_toolboxes[tab_toolbox.get_tab_toolbox_name()] = tab_toolbox
329
+
330
+ # Set reference for plot-tab.
331
+ if isinstance(tab_toolbox, cfclient.ui.tabs.PlotTab):
332
+ cfclient.ui.pluginhelper.plotTab = tab_toolbox
333
+
334
+ # Add to tabs menu
335
+ tab_action_item = QAction(tab_toolbox.get_tab_toolbox_name())
336
+ tab_action_item.setCheckable(True)
337
+ tab_action_item.triggered.connect(self.toggle_tab_visibility)
338
+ tab_action_item.tab_toolbox = tab_toolbox
339
+ tab_toolbox.tab_action_item = tab_action_item
340
+
341
+ tabs_menu_item.addAction(tab_action_item)
342
+
343
+ # Add to toolbox menu
344
+ toolbox_action_item = QAction(tab_toolbox.get_tab_toolbox_name())
345
+ toolbox_action_item.setCheckable(True)
346
+ toolbox_action_item.triggered.connect(self.toggle_toolbox_visibility)
347
+ toolbox_action_item.tab_toolbox = tab_toolbox
348
+ tab_toolbox.toolbox_action_item = toolbox_action_item
349
+ tab_toolbox.dock_widget.closed.connect(lambda: self.toggle_toolbox_visibility(False))
350
+ tab_toolbox.dock_widget.dockLocationChanged.connect(lambda area: self.set_preferred_dock_area(area))
351
+
352
+ toolboxes_menu_item.addAction(toolbox_action_item)
353
+
354
+ return loaded_tab_toolboxes
355
+
356
+ def read_tab_toolbox_config(self, loaded_tab_toolboxes):
357
+ # Add tabs in the correct order
358
+ for name in TabToolbox.read_open_tab_config():
359
+ if name in loaded_tab_toolboxes.keys():
360
+ self._tab_toolbox_show_as_tab(loaded_tab_toolboxes[name])
361
+
362
+ for name in TabToolbox.read_open_toolbox_config():
363
+ if name in loaded_tab_toolboxes.keys():
364
+ self._tab_toolbox_show_as_toolbox(loaded_tab_toolboxes[name])
365
+
366
+ def _set_address(self):
367
+ address = 0xE7E7E7E7E7
368
+ try:
369
+ link_uri = Config().get("link_uri")
370
+ if link_uri.startswith("radio://"):
371
+ if len(link_uri) > 0:
372
+ parts = link_uri.split('/')
373
+ # The uri might not contain an address
374
+ if len(parts) == 6:
375
+ address = int(parts[-1], 16)
376
+ except Exception as err:
377
+ logger.warn('failed to parse address from config: %s' % str(err))
378
+ finally:
379
+ self.address.setValue(address)
380
+
381
+ def _theme_selected(self, *args):
382
+ """ Callback when a theme is selected. """
383
+ for checkbox in self._theme_checkboxes:
384
+ if checkbox.isChecked():
385
+ theme = checkbox.objectName()
386
+ app = QtWidgets.QApplication.instance()
387
+ app.setStyleSheet(UiUtils.select_theme(theme))
388
+ Config().set('theme', theme)
389
+
390
+ def _check_theme(self, theme_name):
391
+ # Check the default theme.
392
+ for theme in self._theme_checkboxes:
393
+ if theme.objectName() == theme_name:
394
+ theme.setChecked(True)
395
+ self._theme_selected(True)
396
+
397
+ def set_default_theme(self):
398
+ try:
399
+ theme = Config().get('theme')
400
+ except KeyError:
401
+ theme = 'Default'
402
+ self._check_theme(theme)
395
403
 
396
- def foundInterfaces(self, interfaces):
397
- selected_interface = self._selected_interface
404
+ def disable_input(self, disable):
405
+ """
406
+ Disable the gamepad input to be able to send setpoint from a tab
407
+ """
408
+ self._disable_input = disable
398
409
 
399
- self.interfaceCombo.clear()
400
- self.interfaceCombo.addItem(INTERFACE_PROMPT_TEXT)
410
+ def foundInterfaces(self, interfaces):
411
+ selected_interface = self._connectivity_manager.get_interface()
401
412
 
402
413
  formatted_interfaces = []
403
414
  for i in interfaces:
@@ -406,7 +417,6 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
406
417
  else:
407
418
  interface = i[0]
408
419
  formatted_interfaces.append(interface)
409
- self.interfaceCombo.addItems(formatted_interfaces)
410
420
 
411
421
  if self._initial_scan:
412
422
  self._initial_scan = False
@@ -425,14 +435,14 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
425
435
  if len(interfaces) == 1 and selected_interface is None:
426
436
  selected_interface = interfaces[0][0]
427
437
 
428
- newIndex = 0
438
+ newIndex = None
429
439
  if selected_interface is not None:
430
440
  try:
431
- newIndex = formatted_interfaces.index(selected_interface) + 1
441
+ newIndex = formatted_interfaces.index(selected_interface)
432
442
  except ValueError:
433
443
  pass
434
444
 
435
- self.interfaceCombo.setCurrentIndex(newIndex)
445
+ self._connectivity_manager.set_interfaces(formatted_interfaces, newIndex)
436
446
 
437
447
  self.uiState = UIState.DISCONNECTED
438
448
  self._update_ui_state()
@@ -440,75 +450,108 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
440
450
  def _update_ui_state(self):
441
451
  if self.uiState == UIState.DISCONNECTED:
442
452
  self.setWindowTitle("Not connected")
443
- canConnect = self._selected_interface is not None
453
+ canConnect = self._connectivity_manager.get_interface() is not None
444
454
  self.menuItemConnect.setText("Connect to Crazyflie")
445
455
  self.menuItemConnect.setEnabled(canConnect)
446
- self.connectButton.setText("Connect")
447
- self.connectButton.setToolTip(
448
- "Connect to the Crazyflie on the selected interface")
449
- self.connectButton.setEnabled(canConnect)
450
- self.scanButton.setText("Scan")
451
- self.scanButton.setEnabled(True)
452
- self.address.setEnabled(True)
456
+ self._connectivity_manager.set_state(ConnectivityManager.UIState.DISCONNECTED)
453
457
  self.batteryBar.setValue(3000)
454
458
  self._menu_cf2_config.setEnabled(False)
455
- self._menu_cf1_config.setEnabled(True)
456
459
  self.linkQualityBar.setValue(0)
457
- self.menuItemBootloader.setEnabled(True)
458
460
  self.logConfigAction.setEnabled(False)
459
- self.interfaceCombo.setEnabled(True)
461
+ self.esButton.setStyleSheet("")
462
+ self.esButton.setEnabled(False)
460
463
  elif self.uiState == UIState.CONNECTED:
461
- s = "Connected on %s" % self._selected_interface
464
+ s = "Connected on %s" % self._connectivity_manager.get_interface()
462
465
  self.setWindowTitle(s)
463
466
  self.menuItemConnect.setText("Disconnect")
464
467
  self.menuItemConnect.setEnabled(True)
465
- self.connectButton.setText("Disconnect")
466
- self.connectButton.setToolTip("Disconnect from the Crazyflie")
467
- self.scanButton.setEnabled(False)
468
+ self._connectivity_manager.set_state(ConnectivityManager.UIState.CONNECTED)
468
469
  self.logConfigAction.setEnabled(True)
470
+ self.esButton.setEnabled(True)
471
+ self.esButton.setStyleSheet("background-color: red")
469
472
  # Find out if there's an I2C EEPROM, otherwise don't show the
470
473
  # dialog.
471
474
  if len(self.cf.mem.get_mems(MemoryElement.TYPE_I2C)) > 0:
472
475
  self._menu_cf2_config.setEnabled(True)
473
- self._menu_cf1_config.setEnabled(False)
474
476
  elif self.uiState == UIState.CONNECTING:
475
- s = "Connecting to {} ...".format(self._selected_interface)
477
+ s = "Connecting to {} ...".format(self._connectivity_manager.get_interface())
476
478
  self.setWindowTitle(s)
477
479
  self.menuItemConnect.setText("Cancel")
478
480
  self.menuItemConnect.setEnabled(True)
479
- self.connectButton.setText("Cancel")
480
- self.connectButton.setToolTip("Cancel connecting to the Crazyflie")
481
- self.scanButton.setEnabled(False)
482
- self.address.setEnabled(False)
483
- self.menuItemBootloader.setEnabled(False)
484
- self.interfaceCombo.setEnabled(False)
481
+ self._connectivity_manager.set_state(ConnectivityManager.UIState.CONNECTING)
485
482
  elif self.uiState == UIState.SCANNING:
486
483
  self.setWindowTitle("Scanning ...")
487
- self.connectButton.setText("Connect")
488
484
  self.menuItemConnect.setEnabled(False)
489
- self.connectButton.setText("Connect")
490
- self.connectButton.setEnabled(False)
491
- self.scanButton.setText("Scanning...")
492
- self.scanButton.setEnabled(False)
493
- self.address.setEnabled(False)
494
- self.menuItemBootloader.setEnabled(False)
495
- self.interfaceCombo.setEnabled(False)
485
+ self._connectivity_manager.set_state(ConnectivityManager.UIState.SCANNING)
496
486
 
497
487
  @pyqtSlot(bool)
498
- def toggleToolbox(self, display):
499
- menuItem = self.sender().menuItem
500
- dockToolbox = self.sender().dockToolbox
501
-
502
- if display and not dockToolbox.isVisible():
503
- dockToolbox.widget().enable()
504
- self.addDockWidget(dockToolbox.widget().preferedDockArea(),
505
- dockToolbox)
506
- dockToolbox.show()
507
- elif not display:
508
- dockToolbox.widget().disable()
509
- self.removeDockWidget(dockToolbox)
510
- dockToolbox.hide()
511
- menuItem.setChecked(False)
488
+ def toggle_tab_visibility(self, checked):
489
+ tab_action_item = self.sender()
490
+ tab_toolbox = tab_action_item.tab_toolbox
491
+
492
+ if checked:
493
+ self._tab_toolbox_show_as_tab(tab_toolbox)
494
+ else:
495
+ self._tab_toolbox_hide(tab_toolbox)
496
+
497
+ @pyqtSlot(bool)
498
+ def toggle_toolbox_visibility(self, checked):
499
+ toolbox_action_item = self.sender()
500
+ tab_toolbox = toolbox_action_item.tab_toolbox
501
+
502
+ if checked:
503
+ self._tab_toolbox_show_as_toolbox(tab_toolbox)
504
+ else:
505
+ self._tab_toolbox_hide(tab_toolbox)
506
+
507
+ def _tab_toolbox_show_as_tab(self, tab_toolbox):
508
+ if tab_toolbox.get_display_state() == TabToolbox.DS_TOOLBOX:
509
+ dock_widget = tab_toolbox.dock_widget
510
+ self.removeDockWidget(dock_widget)
511
+ dock_widget.hide()
512
+
513
+ if tab_toolbox.get_display_state() != TabToolbox.DS_TAB:
514
+ tab_toolbox_name = tab_toolbox.get_tab_toolbox_name()
515
+ self.tab_widget.addTab(tab_toolbox, tab_toolbox_name)
516
+
517
+ tab_toolbox.tab_action_item.setChecked(True)
518
+ tab_toolbox.toolbox_action_item.setChecked(False)
519
+ tab_toolbox.set_display_state(TabToolbox.DS_TAB)
520
+
521
+ def _tab_toolbox_show_as_toolbox(self, tab_toolbox):
522
+ dock_widget = tab_toolbox.dock_widget
523
+
524
+ if tab_toolbox.get_display_state() == TabToolbox.DS_TAB:
525
+ self.tab_widget.removeTab(self.tab_widget.indexOf(tab_toolbox))
526
+
527
+ if tab_toolbox.get_display_state() != TabToolbox.DS_TOOLBOX:
528
+ self.addDockWidget(tab_toolbox.preferred_dock_area(), dock_widget)
529
+ dock_widget.setWidget(tab_toolbox)
530
+ dock_widget.show()
531
+
532
+ tab_toolbox.tab_action_item.setChecked(False)
533
+ tab_toolbox.toolbox_action_item.setChecked(True)
534
+ tab_toolbox.set_display_state(TabToolbox.DS_TOOLBOX)
535
+
536
+ def _tab_toolbox_hide(self, tab_toolbox):
537
+ dock_widget = tab_toolbox.dock_widget
538
+
539
+ if tab_toolbox.get_display_state() == TabToolbox.DS_TAB:
540
+ self.tab_widget.removeTab(self.tab_widget.indexOf(tab_toolbox))
541
+ elif tab_toolbox.get_display_state() == TabToolbox.DS_TOOLBOX:
542
+ self.removeDockWidget(dock_widget)
543
+ dock_widget.hide()
544
+ tab_toolbox.toolbox_action_item.setChecked(False)
545
+
546
+ tab_toolbox.tab_action_item.setChecked(False)
547
+ tab_toolbox.toolbox_action_item.setChecked(False)
548
+ tab_toolbox.set_display_state(TabToolbox.DS_HIDDEN)
549
+
550
+ @pyqtSlot(Qt.DockWidgetArea)
551
+ def set_preferred_dock_area(self, area):
552
+ dock_widget = self.sender()
553
+ tab_toolbox = dock_widget.tab_toolbox
554
+ tab_toolbox.set_preferred_dock_area(area)
512
555
 
513
556
  def _rescan_devices(self):
514
557
  self._statusbar_label.setText("No inputdevice connected!")
@@ -526,32 +569,35 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
526
569
  self.inputConfig = InputConfigDialogue(self.joystickReader)
527
570
  self.inputConfig.show()
528
571
 
529
- def _auto_reconnect_changed(self, checked):
530
- self._auto_reconnect_enabled = checked
531
- Config().set("auto_reconnect", checked)
532
- logger.info("Auto reconnect enabled: {}".format(checked))
533
-
534
572
  def _show_connect_dialog(self):
535
573
  self.logConfigDialogue.show()
536
574
 
575
+ def _emergency_stop(self):
576
+ # send disarming command
577
+ if (self.uiState == UIState.CONNECTED):
578
+ # Send both emergency stop and disarm
579
+ # TODO krri Disarm?
580
+ self.cf.loc.send_emergency_stop()
581
+
537
582
  def _update_battery(self, timestamp, data, logconf):
538
583
  self.batteryBar.setValue(int(data["pm.vbat"] * 1000))
539
584
 
540
- color = COLOR_BLUE
585
+ color = UiUtils.COLOR_BLUE
541
586
  # TODO firmware reports fully-charged state as 'Battery',
542
587
  # rather than 'Charged'
543
588
  if data["pm.state"] in [BatteryStates.CHARGING, BatteryStates.CHARGED]:
544
- color = COLOR_GREEN
589
+ color = UiUtils.COLOR_GREEN
545
590
  elif data["pm.state"] == BatteryStates.LOW_POWER:
546
- color = COLOR_RED
591
+ color = UiUtils.COLOR_RED
547
592
 
548
- self.batteryBar.setStyleSheet(progressbar_stylesheet(color))
593
+ self.batteryBar.setStyleSheet(UiUtils.progressbar_stylesheet(color))
594
+ self._aff_volts.setText(("%.3f" % data["pm.vbat"]))
549
595
 
550
596
  def _connected(self):
551
597
  self.uiState = UIState.CONNECTED
552
598
  self._update_ui_state()
553
599
 
554
- Config().set("link_uri", str(self._selected_interface))
600
+ Config().set("link_uri", str(self._connectivity_manager.get_interface()))
555
601
 
556
602
  lg = LogConfig("Battery", 1000)
557
603
  lg.add_variable("pm.vbat", "float")
@@ -585,30 +631,24 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
585
631
  msg))
586
632
 
587
633
  def _connection_lost(self, linkURI, msg):
588
- if not self._auto_reconnect_enabled:
589
- if self.isActiveWindow():
590
- warningCaption = "Communication failure"
591
- error = "Connection lost to {}: {}".format(linkURI, msg)
592
- QMessageBox.critical(self, warningCaption, error)
593
- self.uiState = UIState.DISCONNECTED
594
- self._update_ui_state()
595
- else:
596
- self._connect()
597
-
598
- def _connection_failed(self, linkURI, error):
599
- if not self._auto_reconnect_enabled:
600
- msg = "Failed to connect on {}: {}".format(linkURI, error)
634
+ if self.isActiveWindow():
601
635
  warningCaption = "Communication failure"
602
- QMessageBox.critical(self, warningCaption, msg)
636
+ error = "Connection lost to {}: {}".format(linkURI, msg)
637
+ QMessageBox.critical(self, warningCaption, error)
603
638
  self.uiState = UIState.DISCONNECTED
604
639
  self._update_ui_state()
605
- else:
606
- self._connect()
640
+
641
+ def _connection_failed(self, linkURI, error):
642
+ msg = "Failed to connect on {}: {}".format(linkURI, error)
643
+ warningCaption = "Communication failure"
644
+ QMessageBox.critical(self, warningCaption, msg)
645
+ self.uiState = UIState.DISCONNECTED
646
+ self._update_ui_state()
607
647
 
608
648
  def closeEvent(self, event):
609
- self.hide()
610
- self.cf.close_link()
611
649
  Config().save_file()
650
+ self.cf.close_link()
651
+ self.hide()
612
652
 
613
653
  def resizeEvent(self, event):
614
654
  Config().set("window_size", [event.size().width(),
@@ -622,12 +662,40 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
622
662
  self.uiState = UIState.DISCONNECTED
623
663
  self._update_ui_state()
624
664
  else:
625
- self.cf.open_link(self._selected_interface)
665
+ self.cf.open_link(self._connectivity_manager.get_interface())
626
666
 
627
- def _scan(self):
667
+ def _scan(self, address):
628
668
  self.uiState = UIState.SCANNING
629
669
  self._update_ui_state()
630
- self.scanner.scanSignal.emit(self.address.value())
670
+ self.scanner.scanSignal.emit(address)
671
+
672
+ def _scan_from_button(self, address):
673
+ #
674
+ # Below we check if we can open the Crazyradio device.
675
+ # If it is there, but we have no permissions we inform the user, once,
676
+ # about how to install the udev rules.
677
+ #
678
+ if not self._permission_warned:
679
+ try:
680
+ radio = cflib.crtp.radiodriver.RadioManager.open(0)
681
+ radio.close()
682
+ except usb.core.USBError as e:
683
+ if e.errno == 13: # Permission denied
684
+ link = "<a href='https://www.bitcraze.io/documentation/repository/crazyflie-lib-python/master/installation/usb_permissions/'>Install USB Permissions</a>" # noqa
685
+ msg = QMessageBox()
686
+ msg.setIcon(QMessageBox.Icon.Information)
687
+ msg.setTextFormat(Qt.TextFormat.RichText)
688
+ msg.setText("Could not access Crazyradio")
689
+ msg.setInformativeText(link)
690
+ msg.setWindowTitle("Crazyradio permissions")
691
+ msg.exec()
692
+ self._permission_warned = True
693
+ except Exception as e:
694
+ # For other Crazyradio exceptions (for instance if it's not attached)
695
+ # ignore and keep scanning other link drivers.
696
+ logger.warning(e)
697
+
698
+ self._scan(address)
631
699
 
632
700
  def _display_input_device_error(self, error):
633
701
  self.cf.close_link()
@@ -659,7 +727,7 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
659
727
  def _get_dev_status(self, device):
660
728
  msg = "{}".format(device.name)
661
729
  if device.supports_mapping:
662
- map_name = "N/A"
730
+ map_name = "No input mapping"
663
731
  if device.input_map:
664
732
  map_name = device.input_map_name
665
733
  msg += " ({})".format(map_name)
@@ -725,8 +793,8 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
725
793
 
726
794
  def _inputconfig_selected(self, checked):
727
795
  """Called when a new configuration has been selected from the menu. The
728
- data in the menu object is a referance to the device QAction in parent
729
- menu. This contains a referance to the raw device."""
796
+ data in the menu object is a reference to the device QAction in parent
797
+ menu. This contains a reference to the raw device."""
730
798
  if not checked:
731
799
  return
732
800
 
@@ -740,7 +808,8 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
740
808
  for menu in self._all_role_menus:
741
809
  role_menu = menu["rolemenu"]
742
810
  mux_menu = menu["muxmenu"]
743
- dev_group = QActionGroup(role_menu, exclusive=True)
811
+ dev_group = QActionGroup(role_menu)
812
+ dev_group.setExclusive(True)
744
813
  for d in devs:
745
814
  dev_node = QAction(d.name, role_menu, checkable=True,
746
815
  enabled=True)
@@ -751,7 +820,8 @@ class MainUI(QtWidgets.QMainWindow, main_window_class):
751
820
  map_node = None
752
821
  if d.supports_mapping:
753
822
  map_node = QMenu(" Input map", role_menu, enabled=False)
754
- map_group = QActionGroup(role_menu, exclusive=True)
823
+ map_group = QActionGroup(role_menu)
824
+ map_group.setExclusive(True)
755
825
  # Connect device node to map node for easy
756
826
  # enabling/disabling when selection changes and device
757
827
  # to easily enable it