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
@@ -0,0 +1,714 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # || ____ _ __
5
+ # +------+ / __ )(_) /_______________ _____ ___
6
+ # | 0xBC | / __ / / __/ ___/ ___/ __ `/_ / / _ \
7
+ # +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
8
+ # || || /_____/_/\__/\___/_/ \__,_/ /___/\___/
9
+ #
10
+ # Copyright (C) 2022-2023 Bitcraze AB
11
+ #
12
+ # Crazyflie Nano Quadcopter Client
13
+ #
14
+ # This program is free software; you can redistribute it and/or
15
+ # modify it under the terms of the GNU General Public License
16
+ # as published by the Free Software Foundation; either version 2
17
+ # of the License, or (at your option) any later version.
18
+ #
19
+ # This program is distributed in the hope that it will be useful,
20
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
21
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
+ # GNU General Public License for more details.
23
+
24
+ # You should have received a copy of the GNU General Public License
25
+ # along with this program; if not, write to the Free Software
26
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27
+ # 02110-1301, USA.
28
+
29
+ """
30
+ Shows data for the Lighthouse Positioning system
31
+ """
32
+
33
+ import logging
34
+
35
+ from PyQt6 import uic
36
+ from PyQt6.QtCore import Qt, pyqtSignal, QTimer
37
+ from PyQt6.QtWidgets import QMessageBox
38
+ from PyQt6.QtWidgets import QFileDialog
39
+ from PyQt6.QtWidgets import QLabel
40
+
41
+ import cfclient
42
+ from cfclient.ui.tab_toolbox import TabToolbox
43
+
44
+ from cflib.crazyflie.log import LogConfig
45
+ from cflib.crazyflie.mem import LighthouseMemHelper
46
+ from cflib.localization import LighthouseConfigWriter
47
+ from cflib.localization import LighthouseConfigFileManager
48
+
49
+ from cfclient.ui.dialogs.lighthouse_bs_geometry_dialog import LighthouseBsGeometryDialog
50
+ from cfclient.ui.dialogs.basestation_mode_dialog import LighthouseBsModeDialog
51
+ from cfclient.ui.dialogs.lighthouse_system_type_dialog import LighthouseSystemTypeDialog
52
+ from cfclient.utils.logconfigreader import FILE_REGEX_YAML
53
+
54
+ from vispy import scene
55
+ import numpy as np
56
+ import math
57
+ import os
58
+
59
+ __author__ = 'Bitcraze AB'
60
+ __all__ = ['LighthouseTab']
61
+
62
+ logger = logging.getLogger(__name__)
63
+
64
+ lighthouse_tab_class = uic.loadUiType(cfclient.module_path + "/ui/tabs/lighthouse_tab.ui")[0]
65
+
66
+ STYLE_RED_BACKGROUND = "background-color: lightpink;"
67
+ STYLE_GREEN_BACKGROUND = "background-color: lightgreen;"
68
+ STYLE_BLUE_BACKGROUND = "background-color: lightblue;"
69
+ STYLE_ORANGE_BACKGROUND = "background-color: orange;"
70
+ STYLE_NO_BACKGROUND = "background-color: none;"
71
+
72
+
73
+ class MarkerPose():
74
+ COL_X_AXIS = 'red'
75
+ COL_Y_AXIS = 'green'
76
+ COL_Z_AXIS = 'blue'
77
+
78
+ AXIS_LEN = 0.3
79
+
80
+ LABEL_SIZE = 100
81
+ LABEL_OFFSET = np.array((0.0, 0, 0.25))
82
+
83
+ def __init__(self, the_scene, color, text=None):
84
+ self._scene = the_scene
85
+ self._color = color
86
+ self._text = text
87
+
88
+ self._marker = scene.visuals.Markers(
89
+ pos=np.array([[0, 0, 0]]),
90
+ parent=self._scene,
91
+ face_color=self._color)
92
+
93
+ self._x_axis = scene.visuals.Line(
94
+ pos=np.array([[0, 0, 0], [0, 0, 0]]),
95
+ color=self.COL_X_AXIS,
96
+ parent=self._scene)
97
+
98
+ self._y_axis = scene.visuals.Line(pos=np.array(
99
+ [[0, 0, 0], [0, 0, 0]]),
100
+ color=self.COL_Y_AXIS,
101
+ parent=self._scene)
102
+
103
+ self._z_axis = scene.visuals.Line(
104
+ pos=np.array([[0, 0, 0], [0, 0, 0]]),
105
+ color=self.COL_Z_AXIS,
106
+ parent=self._scene)
107
+
108
+ self._label = None
109
+ if self._text:
110
+ self._label = scene.visuals.Text(
111
+ text=self._text,
112
+ font_size=self.LABEL_SIZE,
113
+ pos=self.LABEL_OFFSET,
114
+ parent=self._scene)
115
+
116
+ def set_pose(self, position, rot):
117
+ self._marker.set_data(pos=np.array([position]), face_color=self._color)
118
+
119
+ if self._label:
120
+ self._label.pos = self.LABEL_OFFSET + position
121
+
122
+ x_tip = np.dot(np.array(rot), np.array([self.AXIS_LEN, 0, 0]))
123
+ self._x_axis.set_data(np.array([position, x_tip + position]), color=self.COL_X_AXIS)
124
+
125
+ y_tip = np.dot(np.array(rot), np.array([0, self.AXIS_LEN, 0]))
126
+ self._y_axis.set_data(np.array([position, y_tip + position]), color=self.COL_Y_AXIS)
127
+
128
+ z_tip = np.dot(np.array(rot), np.array([0, 0, self.AXIS_LEN]))
129
+ self._z_axis.set_data(np.array([position, z_tip + position]), color=self.COL_Z_AXIS)
130
+
131
+ def remove(self):
132
+ self._marker.parent = None
133
+ self._x_axis.parent = None
134
+ self._y_axis.parent = None
135
+ self._z_axis.parent = None
136
+ if self._label:
137
+ self._label.parent = None
138
+
139
+ def set_color(self, color):
140
+ self._color = color
141
+ self._marker.set_data(face_color=self._color)
142
+
143
+
144
+ class Plot3dLighthouse(scene.SceneCanvas):
145
+ POSITION_BRUSH = np.array((0, 0, 1.0))
146
+ BS_BRUSH_VISIBLE = np.array((0.2, 0.5, 0.2))
147
+ BS_BRUSH_NOT_VISIBLE = np.array((0.8, 0.5, 0.5))
148
+
149
+ VICINITY_DISTANCE = 2.5
150
+ HIGHLIGHT_DISTANCE = 0.5
151
+
152
+ LABEL_SIZE = 100
153
+ LABEL_HIGHLIGHT_SIZE = 200
154
+
155
+ HIGHLIGHT_SIZE = 20
156
+
157
+ TEXT_OFFSET = np.array((0.0, 0, 0.25))
158
+
159
+ def __init__(self):
160
+ scene.SceneCanvas.__init__(self, keys=None)
161
+ self.unfreeze()
162
+
163
+ self._view = self.central_widget.add_view()
164
+ self._view.bgcolor = '#ffffff'
165
+ self._view.camera = scene.TurntableCamera(
166
+ distance=10.0,
167
+ up='+z',
168
+ center=(0.0, 0.0, 1.0))
169
+
170
+ self._cf = None
171
+ self._base_stations = {}
172
+
173
+ self.freeze()
174
+
175
+ plane_size = 10
176
+ scene.visuals.Plane(
177
+ width=plane_size,
178
+ height=plane_size,
179
+ width_segments=plane_size,
180
+ height_segments=plane_size,
181
+ color=(0.5, 0.5, 0.5, 0.5),
182
+ edge_color="gray",
183
+ parent=self._view.scene)
184
+
185
+ self._addArrows(1, 0.02, 0.1, 0.1, self._view.scene)
186
+
187
+ def _addArrows(self, length, width, head_length, head_width, parent):
188
+ # The Arrow visual in vispy does not seem to work very good,
189
+ # draw arrows using lines instead.
190
+ w = width / 2
191
+ hw = head_width / 2
192
+ base_len = length - head_length
193
+
194
+ # X-axis
195
+ scene.visuals.LinePlot([
196
+ [0, w, 0],
197
+ [base_len, w, 0],
198
+ [base_len, hw, 0],
199
+ [length, 0, 0],
200
+ [base_len, -hw, 0],
201
+ [base_len, -w, 0],
202
+ [0, -w, 0]],
203
+ width=1.0, color='red', parent=parent, marker_size=0.0)
204
+
205
+ # Y-axis
206
+ scene.visuals.LinePlot([
207
+ [w, 0, 0],
208
+ [w, base_len, 0],
209
+ [hw, base_len, 0],
210
+ [0, length, 0],
211
+ [-hw, base_len, 0],
212
+ [-w, base_len, 0],
213
+ [-w, 0, 0]],
214
+ width=1.0, color='green', parent=parent, marker_size=0.0)
215
+
216
+ # Z-axis
217
+ scene.visuals.LinePlot([
218
+ [0, w, 0],
219
+ [0, w, base_len],
220
+ [0, hw, base_len],
221
+ [0, 0, length],
222
+ [0, -hw, base_len],
223
+ [0, -w, base_len],
224
+ [0, -w, 0]],
225
+ width=1.0, color='blue', parent=parent, marker_size=0.0)
226
+
227
+ def update_cf_pose(self, position, rot):
228
+ if not self._cf:
229
+ self._cf = MarkerPose(self._view.scene, self.POSITION_BRUSH)
230
+ self._cf.set_pose(position, rot)
231
+
232
+ def update_base_station_geos(self, geos):
233
+ for id, geo in geos.items():
234
+ if (geo is not None) and (id not in self._base_stations):
235
+ self._base_stations[id] = MarkerPose(self._view.scene, self.BS_BRUSH_NOT_VISIBLE, text=f"{id + 1}")
236
+ self._base_stations[id].set_pose(geo.origin, geo.rotation_matrix)
237
+
238
+ geos_to_remove = self._base_stations.keys() - geos.keys()
239
+ for id in geos_to_remove:
240
+ existing = self._base_stations.pop(id)
241
+ existing.remove()
242
+
243
+ def update_base_station_visibility(self, visibility):
244
+ for id, bs in self._base_stations.items():
245
+ if id in visibility:
246
+ bs.set_color(self.BS_BRUSH_VISIBLE)
247
+ else:
248
+ bs.set_color(self.BS_BRUSH_NOT_VISIBLE)
249
+
250
+ def clear(self):
251
+ if self._cf:
252
+ self._cf.remove()
253
+ self._cf = None
254
+
255
+ for bs in self._base_stations.values():
256
+ bs.remove()
257
+ self._base_stations = {}
258
+
259
+ def _mix(self, col1, col2, mix):
260
+ return col1 * mix + col2 * (1.0 - mix)
261
+
262
+
263
+ class LighthouseTab(TabToolbox, lighthouse_tab_class):
264
+ """Tab for plotting Lighthouse data"""
265
+
266
+ # Update period of log data in ms
267
+ UPDATE_PERIOD_LOG = 100
268
+
269
+ # Frame rate (updates per second)
270
+ FPS = 2
271
+
272
+ STATUS_NOT_RECEIVING = 0
273
+ STATUS_MISSING_DATA = 1
274
+ STATUS_TO_ESTIMATOR = 2
275
+
276
+ # TODO change these names to something more logical
277
+ LOG_STATUS = "lighthouse.status"
278
+ LOG_RECEIVE = "lighthouse.bsReceive"
279
+ LOG_CALIBRATION_EXISTS = "lighthouse.bsCalVal"
280
+ LOG_CALIBRATION_CONFIRMED = "lighthouse.bsCalCon"
281
+ LOG_CALIBRATION_UPDATED = "lighthouse.bsCalUd"
282
+ LOG_GEOMETERY_EXISTS = "lighthouse.bsGeoVal"
283
+ LOG_ACTIVE = "lighthouse.bsActive"
284
+ LOG_AVAILABLE = "lighthouse.bsAvailable"
285
+
286
+ _connected_signal = pyqtSignal(str)
287
+ _disconnected_signal = pyqtSignal(str)
288
+ _log_error_signal = pyqtSignal(object, str)
289
+ _status_report_signal = pyqtSignal(int, object, object)
290
+ _new_system_config_written_to_cf_signal = pyqtSignal(bool)
291
+ _geometry_read_signal = pyqtSignal(object)
292
+ _calibration_read_signal = pyqtSignal(object)
293
+
294
+ def __init__(self, helper):
295
+ super(LighthouseTab, self).__init__(helper, 'Lighthouse Positioning')
296
+ self.setupUi(self)
297
+
298
+ # Always wrap callbacks from Crazyflie API though QT Signal/Slots
299
+ # to avoid manipulating the UI when rendering it
300
+ self._connected_signal.connect(self._connected)
301
+ self._disconnected_signal.connect(self._disconnected)
302
+ self._log_error_signal.connect(self._logging_error)
303
+ self._status_report_signal.connect(self._status_report_received)
304
+ self._new_system_config_written_to_cf_signal.connect(self._new_system_config_written_to_cf)
305
+ self._geometry_read_signal.connect(self._geometry_read_cb)
306
+ self._calibration_read_signal.connect(self._calibration_read_cb)
307
+
308
+ # Connect the Crazyflie API callbacks to the signals
309
+ self._helper.cf.connected.add_callback(self._connected_signal.emit)
310
+ self._helper.cf.disconnected.add_callback(self._disconnected_signal.emit)
311
+
312
+ self._set_up_plots()
313
+
314
+ self.is_lighthouse_deck_active = False
315
+
316
+ self._lh_memory_helper = None
317
+ self._lh_config_writer = None
318
+ self._lh_geos = {}
319
+ self._is_geometry_read_ongoing = False
320
+
321
+ self._bs_receives_light = set()
322
+ self._bs_calibration_data_exists = set()
323
+ self._bs_calibration_data_confirmed = set()
324
+ self._bs_calibration_data_updated = set()
325
+ self._bs_geometry_data_exists = set()
326
+ self._bs_data_to_estimator = set()
327
+ self._bs_available = set()
328
+
329
+ self._clear_state_indicator()
330
+
331
+ self._bs_stats = [
332
+ self._bs_receives_light,
333
+ self._bs_calibration_data_exists,
334
+ self._bs_calibration_data_confirmed,
335
+ self._bs_calibration_data_updated,
336
+ self._bs_geometry_data_exists,
337
+ self._bs_data_to_estimator,
338
+ self._bs_available]
339
+
340
+ self._lh_status = self.STATUS_NOT_RECEIVING
341
+
342
+ self._graph_timer = QTimer()
343
+ self._graph_timer.setInterval(int(1000 / self.FPS))
344
+ self._graph_timer.timeout.connect(self._update_graphics)
345
+ self._graph_timer.start()
346
+
347
+ self._basestation_geometry_dialog = LighthouseBsGeometryDialog(self)
348
+ self._basestation_mode_dialog = LighthouseBsModeDialog(self)
349
+ self._system_type_dialog = LighthouseSystemTypeDialog(helper)
350
+
351
+ self._manage_estimate_geometry_button.clicked.connect(self._show_basestation_geometry_dialog)
352
+ self._change_system_type_button.clicked.connect(lambda: self._system_type_dialog.show())
353
+ self._manage_basestation_mode_button.clicked.connect(self._show_basestation_mode_dialog)
354
+
355
+ self._load_sys_config_button.clicked.connect(self._load_sys_config_button_clicked)
356
+ self._save_sys_config_button.clicked.connect(self._save_sys_config_button_clicked)
357
+
358
+ self._is_connected = False
359
+ self._update_ui()
360
+
361
+ def write_and_store_geometry(self, geometries):
362
+ if self._lh_config_writer:
363
+ self._lh_config_writer.write_and_store_config(self._new_system_config_written_to_cf_signal.emit,
364
+ geos=geometries)
365
+
366
+ def _new_system_config_written_to_cf(self, success):
367
+ # Reset the bit fields for calibration data status to get a fresh view
368
+ self._helper.cf.param.set_value("lighthouse.bsCalibReset", '1')
369
+ # New geo data has been written and stored in the CF, read it back to update the UI
370
+ self._start_read_of_geo_data()
371
+
372
+ def _show_basestation_geometry_dialog(self):
373
+ self._basestation_geometry_dialog.reset()
374
+ self._basestation_geometry_dialog.show()
375
+
376
+ def _show_basestation_mode_dialog(self):
377
+ self._basestation_mode_dialog.reset()
378
+ self._basestation_mode_dialog.show()
379
+
380
+ def _set_up_plots(self):
381
+ self._plot_3d = Plot3dLighthouse()
382
+ self._plot_layout.addWidget(self._plot_3d.native)
383
+
384
+ def _connected(self, link_uri):
385
+ """Callback when the Crazyflie has been connected"""
386
+ logger.debug("Crazyflie connected to {}".format(link_uri))
387
+
388
+ self._basestation_geometry_dialog.reset()
389
+ self._is_connected = True
390
+
391
+ if self._helper.cf.param.get_value('deck.bcLighthouse4') == '1':
392
+ self._lighthouse_deck_detected()
393
+
394
+ self._update_ui()
395
+
396
+ def _lighthouse_deck_detected(self):
397
+ """Called when the lighthouse deck has been detected. Enables the tab,
398
+ starts logging and polling of the memory sub system as well as starts
399
+ timers for updating graphics"""
400
+ if not self.is_lighthouse_deck_active:
401
+ self.is_lighthouse_deck_active = True
402
+
403
+ try:
404
+ self._register_logblock(
405
+ "lhStatus",
406
+ [self.LOG_STATUS, self.LOG_RECEIVE, self.LOG_CALIBRATION_EXISTS, self.LOG_CALIBRATION_CONFIRMED,
407
+ self.LOG_CALIBRATION_UPDATED, self.LOG_GEOMETERY_EXISTS, self.LOG_ACTIVE, self.LOG_AVAILABLE],
408
+ self._status_report_signal.emit,
409
+ self._log_error_signal.emit)
410
+ except KeyError as e:
411
+ logger.warning(str(e))
412
+ except AttributeError as e:
413
+ logger.warning(str(e))
414
+
415
+ self._populate_status_matrix()
416
+
417
+ # Now that we know we have a lighthouse deck, setup the memory helper and config writer
418
+ self._lh_memory_helper = LighthouseMemHelper(self._helper.cf)
419
+ self._lh_config_writer = LighthouseConfigWriter(self._helper.cf)
420
+
421
+ def _start_read_of_geo_data(self):
422
+ if not self._is_geometry_read_ongoing:
423
+ self._is_geometry_read_ongoing = True
424
+ self._lh_memory_helper.read_all_geos(self._geometry_read_signal.emit)
425
+
426
+ def _geometry_read_cb(self, geometries):
427
+ # Remove any geo data where the valid flag is False
428
+ self._lh_geos = dict(filter(lambda key_value: key_value[1].valid, geometries.items()))
429
+ self._basestation_geometry_dialog.geometry_updated(self._lh_geos)
430
+ self._is_geometry_read_ongoing = False
431
+
432
+ def _is_matching_current_geo_data(self, geometries):
433
+ return geometries == self._lh_geos.keys()
434
+
435
+ def _adjust_bitmask(self, bit_mask, bs_list):
436
+ for id in range(16):
437
+ if bit_mask & (1 << id):
438
+ bs_list.add(id)
439
+ else:
440
+ if id in bs_list:
441
+ bs_list.remove(id)
442
+
443
+ def _status_report_received(self, timestamp, data, logconf):
444
+ """Callback from the logging system when the status is updated."""
445
+
446
+ if self.LOG_RECEIVE in data:
447
+ bit_mask = data[self.LOG_RECEIVE]
448
+ self._adjust_bitmask(bit_mask, self._bs_receives_light)
449
+ if self.LOG_CALIBRATION_EXISTS in data:
450
+ bit_mask = data[self.LOG_CALIBRATION_EXISTS]
451
+ self._adjust_bitmask(bit_mask, self._bs_calibration_data_exists)
452
+ if self.LOG_CALIBRATION_CONFIRMED in data:
453
+ bit_mask = data[self.LOG_CALIBRATION_CONFIRMED]
454
+ self._adjust_bitmask(bit_mask, self._bs_calibration_data_confirmed)
455
+ if self.LOG_CALIBRATION_UPDATED in data:
456
+ bit_mask = data[self.LOG_CALIBRATION_UPDATED]
457
+ self._adjust_bitmask(bit_mask, self._bs_calibration_data_updated)
458
+ if self.LOG_GEOMETERY_EXISTS in data:
459
+ bit_mask = data[self.LOG_GEOMETERY_EXISTS]
460
+ self._adjust_bitmask(bit_mask, self._bs_geometry_data_exists)
461
+ if not self._is_matching_current_geo_data(self._bs_geometry_data_exists):
462
+ self._start_read_of_geo_data()
463
+
464
+ if self.LOG_ACTIVE in data:
465
+ bit_mask = data[self.LOG_ACTIVE]
466
+ self._adjust_bitmask(bit_mask, self._bs_data_to_estimator)
467
+
468
+ if self.LOG_STATUS in data:
469
+ self._lh_status = data[self.LOG_STATUS]
470
+
471
+ if self.LOG_AVAILABLE in data:
472
+ bit_mask = data[self.LOG_AVAILABLE]
473
+ self._adjust_bitmask(bit_mask, self._bs_available)
474
+
475
+ self._update_basestation_status_indicators()
476
+
477
+ def _disconnected(self, link_uri):
478
+ """Callback for when the Crazyflie has been disconnected"""
479
+ logger.debug("Crazyflie disconnected from {}".format(link_uri))
480
+ self._clear_state()
481
+ self._update_graphics()
482
+ self._plot_3d.clear()
483
+ self._basestation_geometry_dialog.close()
484
+ self.is_lighthouse_deck_active = False
485
+ self._is_connected = False
486
+ self._update_ui()
487
+
488
+ def _register_logblock(self, logblock_name, variables, data_cb, error_cb,
489
+ update_period=UPDATE_PERIOD_LOG):
490
+ """Register log data to listen for. One logblock can only contain a limited
491
+ number of parameters."""
492
+ lg = LogConfig(logblock_name, update_period)
493
+ for variable in variables:
494
+ if self._is_in_log_toc(variable):
495
+ lg.add_variable(variable)
496
+
497
+ self._helper.cf.log.add_config(lg)
498
+ lg.data_received_cb.add_callback(data_cb)
499
+ lg.error_cb.add_callback(error_cb)
500
+ lg.start()
501
+ return lg
502
+
503
+ def _is_in_log_toc(self, variable):
504
+ toc = self._helper.cf.log.toc
505
+ group, param = variable.split('.')
506
+ return group in toc.toc and param in toc.toc[group]
507
+
508
+ def _is_in_param_toc(self, group, param):
509
+ toc = self._helper.cf.param.toc
510
+ return bool(group in toc.toc and param in toc.toc[group])
511
+
512
+ def _logging_error(self, log_conf, msg):
513
+ """Callback from the log layer when an error occurs"""
514
+ QMessageBox.about(self, "LighthouseTab error",
515
+ "Error when using log config",
516
+ " [{0}]: {1}".format(log_conf.name, msg))
517
+
518
+ def _update_graphics(self):
519
+ if self.is_visible() and self.is_lighthouse_deck_active:
520
+ self._plot_3d.update_cf_pose(self._helper.pose_logger.position,
521
+ self._rpy_to_rot(self._helper.pose_logger.rpy_rad))
522
+ self._plot_3d.update_base_station_geos(self._lh_geos)
523
+ self._plot_3d.update_base_station_visibility(self._bs_data_to_estimator)
524
+ self._update_position_label(self._helper.pose_logger.position)
525
+ self._update_status_label(self._lh_status)
526
+ self._mask_status_matrix(self._bs_available)
527
+
528
+ def _update_ui(self):
529
+ enabled = self._is_connected and self.is_lighthouse_deck_active
530
+ self._manage_estimate_geometry_button.setEnabled(enabled)
531
+ self._change_system_type_button.setEnabled(enabled)
532
+ self._load_sys_config_button.setEnabled(enabled)
533
+ self._save_sys_config_button.setEnabled(enabled)
534
+
535
+ def _update_position_label(self, position):
536
+ if len(position) == 3:
537
+ coordinate = "({:0.2f}, {:0.2f}, {:0.2f})".format(
538
+ position[0], position[1], position[2])
539
+ else:
540
+ coordinate = '(0.00, 0.00, 0.00)'
541
+
542
+ self._status_position.setText(coordinate)
543
+
544
+ def _update_status_label(self, status):
545
+ text = ''
546
+ if status == self.STATUS_NOT_RECEIVING:
547
+ text = 'Not receiving'
548
+ elif status == self.STATUS_MISSING_DATA:
549
+ text = 'No geo/calib'
550
+ elif status == self.STATUS_TO_ESTIMATOR:
551
+ text = 'LH ready'
552
+
553
+ self._status_status.setText(text)
554
+
555
+ def _clear_state(self):
556
+ self._lh_memory_helper = None
557
+ self._lh_config_writer = None
558
+ self._lh_geos = {}
559
+ self._is_geometry_read_ongoing = False
560
+ self._bs_receives_light.clear()
561
+ self._bs_calibration_data_exists.clear()
562
+ self._bs_calibration_data_confirmed.clear()
563
+ self._bs_calibration_data_updated.clear()
564
+ self._bs_geometry_data_exists.clear()
565
+ self._bs_data_to_estimator.clear()
566
+ self._update_basestation_status_indicators()
567
+ self._clear_state_indicator()
568
+ self._lh_status = self.STATUS_NOT_RECEIVING
569
+
570
+ def _clear_state_indicator(self):
571
+ container = self._basestation_stats_container
572
+ for row in range(0, 5):
573
+ for col in range(1, 17):
574
+ item = container.itemAtPosition(row, col)
575
+ if item is not None:
576
+ item.widget().deleteLater()
577
+
578
+ def _rpy_to_rot(self, rpy):
579
+ # http://planning.cs.uiuc.edu/node102.html
580
+ # Pitch reversed compared to page above
581
+ roll = rpy[0]
582
+ pitch = rpy[1]
583
+ yaw = rpy[2]
584
+
585
+ cg = math.cos(roll)
586
+ cb = math.cos(-pitch)
587
+ ca = math.cos(yaw)
588
+ sg = math.sin(roll)
589
+ sb = math.sin(-pitch)
590
+ sa = math.sin(yaw)
591
+
592
+ r = [
593
+ [ca * cb, ca * sb * sg - sa * cg, ca * sb * cg + sa * sg],
594
+ [sa * cb, sa * sb * sg + ca * cg, sa * sb * cg - ca * sg],
595
+ [-sb, cb * sg, cb * cg],
596
+ ]
597
+
598
+ return np.array(r)
599
+
600
+ def _populate_status_matrix(self):
601
+ container = self._basestation_stats_container
602
+
603
+ # Find the nr of base stations by looking for the highest bit that is set
604
+ # Assume all bs up to that bit are available
605
+ for bs in range(0, 16):
606
+ container.addWidget(self._create_label(str(bs + 1)), 0, bs + 1)
607
+ for i in range(1, 5):
608
+ container.addWidget(self._create_label(), i, bs + 1)
609
+
610
+ def _mask_status_matrix(self, bs_available_mask):
611
+ container = self._basestation_stats_container
612
+
613
+ # Find the nr of base stations by looking for the highest bit that is set
614
+ # Assume all bs up to that bit are available
615
+ for bs in range(0, 16):
616
+ bs_indicator_id = bs + 1
617
+ for stats_indicator_id in range(0, 5):
618
+ item = container.itemAtPosition(stats_indicator_id, bs_indicator_id)
619
+ if item is not None:
620
+ label = item.widget()
621
+ if bs_indicator_id - 1 in bs_available_mask:
622
+ label.setHidden(False)
623
+ else:
624
+ label.setHidden(True)
625
+
626
+ def _create_label(self, text=None):
627
+ label = QLabel()
628
+ label.setMinimumSize(30, 0)
629
+ label.setAlignment(Qt.AlignmentFlag.AlignCenter)
630
+
631
+ if text:
632
+ label.setText(str(text))
633
+ else:
634
+ label.setProperty('frameShape', 'QFrame::Box')
635
+ label.setStyleSheet(STYLE_NO_BACKGROUND)
636
+
637
+ return label
638
+
639
+ def _update_basestation_status_indicators(self):
640
+ """Handling the basestation status label handles to indicate
641
+ the state of received data per basestation"""
642
+ container = self._basestation_stats_container
643
+
644
+ # Ports the label number to the first index of the statistic id
645
+ stats_id_port = {1: 0, 2: 1, 3: 4, 4: 5}
646
+
647
+ for bs in range(16):
648
+ for stats_indicator_id in range(1, 5):
649
+ bs_indicator_id = bs + 1
650
+ item = container.itemAtPosition(stats_indicator_id, bs_indicator_id)
651
+ if item is not None:
652
+ label = item.widget()
653
+ stats_id = stats_id_port.get(stats_indicator_id)
654
+ temp_set = self._bs_stats[stats_id]
655
+
656
+ if bs in temp_set:
657
+ # If the status bar for calibration data is handled, have an intermediate status
658
+ # else just have red or green.
659
+ if stats_indicator_id == 2:
660
+ label.setStyleSheet(STYLE_BLUE_BACKGROUND)
661
+ label.setToolTip('Calibration data from cache')
662
+
663
+ calib_confirm = bs in self._bs_stats[stats_id + 1]
664
+ calib_updated = bs in self._bs_stats[stats_id + 2]
665
+
666
+ if calib_confirm:
667
+ label.setStyleSheet(STYLE_GREEN_BACKGROUND)
668
+ label.setToolTip('Calibration data verified')
669
+ if calib_updated:
670
+ label.setStyleSheet(STYLE_ORANGE_BACKGROUND)
671
+ label.setToolTip('Calibration data updated, the geometry probably needs to be ' +
672
+ 're-estimated')
673
+ else:
674
+ label.setStyleSheet(STYLE_GREEN_BACKGROUND)
675
+ else:
676
+ label.setStyleSheet(STYLE_RED_BACKGROUND)
677
+ label.setToolTip('')
678
+
679
+ def _load_sys_config_button_clicked(self):
680
+ names = QFileDialog.getOpenFileName(self, 'Open file', self._helper.current_folder, FILE_REGEX_YAML)
681
+
682
+ if names[0] == '':
683
+ return
684
+
685
+ self._helper.current_folder = os.path.dirname(names[0])
686
+
687
+ if self._lh_config_writer is not None:
688
+ self._lh_config_writer.write_and_store_config_from_file(self._new_system_config_written_to_cf_signal.emit,
689
+ names[0])
690
+
691
+ def _save_sys_config_button_clicked(self):
692
+ # Get calibration data from the Crazyflie to complete the system config data set
693
+ # When the data is ready we get a callback on _calibration_read
694
+ self._lh_memory_helper.read_all_calibs(self._calibration_read_signal.emit)
695
+
696
+ def _calibration_read_cb(self, calibs):
697
+ # Got calibration data from the CF, we have the full system configuration
698
+ system_type = self._system_type_dialog.get_system_type()
699
+ self._save_sys_config(self._lh_geos, calibs, system_type)
700
+
701
+ def _save_sys_config(self, geos, calibs, system_type):
702
+ names = QFileDialog.getSaveFileName(self, 'Save file', self._helper.current_folder, FILE_REGEX_YAML)
703
+
704
+ if names[0] == '':
705
+ return
706
+
707
+ self._helper.current_folder = os.path.dirname(names[0])
708
+
709
+ if not names[0].endswith(".yaml") and names[0].find(".") < 0:
710
+ filename = names[0] + ".yaml"
711
+ else:
712
+ filename = names[0]
713
+
714
+ LighthouseConfigFileManager.write(filename, geos=geos, calibs=calibs, system_type=system_type)