cfclient 2017.4__py3-none-any.whl → 2025.12.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. cfclient/__init__.py +16 -11
  2. cfclient/configs/config.json +4 -3
  3. cfclient/configs/input/Generic_OS_X.json +1 -0
  4. cfclient/configs/input/Joystick.json +1 -0
  5. cfclient/configs/input/PS3_Mode_1.json +1 -0
  6. cfclient/configs/input/PS3_Mode_2.json +1 -0
  7. cfclient/configs/input/PS3_Mode_3.json +1 -0
  8. cfclient/configs/input/PS4_Mode_1.json +1 -0
  9. cfclient/configs/input/PS4_Mode_2.json +1 -0
  10. cfclient/configs/input/PS4_shoulder_btns_yaw.json +1 -0
  11. cfclient/configs/input/xbox360_mode1.json +1 -0
  12. cfclient/configs/log/PID_tuning/Attitude.json +46 -0
  13. cfclient/configs/log/PID_tuning/Attitude_rate.json +46 -0
  14. cfclient/configs/log/PID_tuning/Position.json +46 -0
  15. cfclient/configs/log/PID_tuning/Velocity.json +46 -0
  16. cfclient/configs/log/PID_tuning_components/Pitch.json +22 -0
  17. cfclient/configs/log/PID_tuning_components/Pitch_rate.json +22 -0
  18. cfclient/configs/log/PID_tuning_components/Position_x.json +22 -0
  19. cfclient/configs/log/PID_tuning_components/Position_y.json +22 -0
  20. cfclient/configs/log/PID_tuning_components/Position_z.json +22 -0
  21. cfclient/configs/log/PID_tuning_components/Roll.json +22 -0
  22. cfclient/configs/log/PID_tuning_components/Roll_rate.json +22 -0
  23. cfclient/configs/log/PID_tuning_components/Velocity_x.json +22 -0
  24. cfclient/configs/log/PID_tuning_components/Velocity_y.json +22 -0
  25. cfclient/configs/log/PID_tuning_components/Velocity_z.json +22 -0
  26. cfclient/configs/log/PID_tuning_components/Yaw.json +22 -0
  27. cfclient/configs/log/PID_tuning_components/Yaw_rate.json +22 -0
  28. cfclient/gui.py +44 -9
  29. cfclient/headless.py +3 -12
  30. cfclient/resources/log_param_doc.json +1 -0
  31. cfclient/ui/connectivity_manager.py +198 -0
  32. cfclient/ui/dialogs/about.py +53 -36
  33. cfclient/ui/dialogs/about.ui +23 -3
  34. cfclient/ui/dialogs/anchor_position_dialog.py +252 -0
  35. cfclient/ui/dialogs/anchor_position_dialog.ui +138 -0
  36. cfclient/ui/dialogs/basestation_mode_dialog.py +185 -0
  37. cfclient/ui/dialogs/basestation_mode_dialog.ui +186 -0
  38. cfclient/ui/dialogs/bootloader.py +448 -85
  39. cfclient/ui/dialogs/bootloader.ui +387 -134
  40. cfclient/ui/dialogs/cf2config.py +4 -4
  41. cfclient/ui/dialogs/cf2config.ui +3 -4
  42. cfclient/ui/dialogs/inputconfigdialogue.py +24 -19
  43. cfclient/ui/dialogs/inputconfigdialogue.ui +53 -30
  44. cfclient/ui/dialogs/lighthouse_bs_geometry_dialog.py +220 -0
  45. cfclient/ui/dialogs/lighthouse_bs_geometry_dialog.ui +110 -0
  46. cfclient/ui/dialogs/lighthouse_system_type_dialog.py +93 -0
  47. cfclient/ui/dialogs/lighthouse_system_type_dialog.ui +121 -0
  48. cfclient/ui/dialogs/logconfigdialogue.py +401 -101
  49. cfclient/ui/dialogs/logconfigdialogue.ui +117 -72
  50. cfclient/ui/icons/bl.webp +0 -0
  51. cfclient/ui/icons/bolt.webp +0 -0
  52. cfclient/ui/icons/cf21.webp +0 -0
  53. cfclient/ui/icons/checkmark_black.png +0 -0
  54. cfclient/ui/icons/checkmark_white.png +0 -0
  55. cfclient/ui/icons/create.png +0 -0
  56. cfclient/ui/icons/delete.png +0 -0
  57. cfclient/ui/icons/flapper.webp +0 -0
  58. cfclient/ui/icons/tag.webp +0 -0
  59. cfclient/ui/main.py +328 -258
  60. cfclient/ui/main.ui +184 -80
  61. cfclient/ui/pluginhelper.py +7 -1
  62. cfclient/ui/pose_logger.py +116 -0
  63. cfclient/ui/tab_toolbox.py +208 -0
  64. cfclient/ui/tabs/ColorLEDTab.py +752 -0
  65. cfclient/ui/tabs/ConsoleTab.py +48 -13
  66. cfclient/ui/{toolboxes → tabs}/CrtpSharkToolbox.py +19 -34
  67. cfclient/ui/tabs/ExampleTab.py +9 -16
  68. cfclient/ui/tabs/FlightTab.py +437 -325
  69. cfclient/ui/tabs/GpsTab.py +14 -20
  70. cfclient/ui/tabs/LEDRingTab.py +277 -0
  71. cfclient/ui/tabs/LogBlockDebugTab.py +20 -27
  72. cfclient/ui/tabs/LogBlockTab.py +35 -35
  73. cfclient/ui/tabs/LogClientTab.py +85 -0
  74. cfclient/ui/tabs/LogTab.py +50 -27
  75. cfclient/ui/tabs/ParamTab.py +443 -57
  76. cfclient/ui/tabs/PlotTab.py +23 -25
  77. cfclient/ui/tabs/TuningTab.py +292 -0
  78. cfclient/ui/tabs/__init__.py +12 -2
  79. cfclient/ui/tabs/colorLEDTab.ui +624 -0
  80. cfclient/ui/tabs/consoleTab.ui +46 -0
  81. cfclient/ui/tabs/flightActionContainer.ui +103 -0
  82. cfclient/ui/tabs/flightTab.ui +724 -237
  83. cfclient/ui/tabs/{ledTab.ui → ledRingTab.ui} +63 -46
  84. cfclient/ui/tabs/lighthouse_tab.py +714 -0
  85. cfclient/ui/tabs/lighthouse_tab.ui +430 -0
  86. cfclient/ui/tabs/locopositioning_tab.py +606 -389
  87. cfclient/ui/tabs/locopositioning_tab.ui +370 -253
  88. cfclient/ui/tabs/logClientTab.ui +52 -0
  89. cfclient/ui/tabs/logTab.ui +1 -1
  90. cfclient/ui/tabs/paramTab.ui +204 -3
  91. cfclient/ui/tabs/tuningTab.ui +773 -0
  92. cfclient/ui/widgets/ai.py +37 -39
  93. cfclient/ui/widgets/hexspinbox.py +16 -10
  94. cfclient/ui/widgets/plotter.ui +39 -47
  95. cfclient/ui/widgets/plotwidget.py +57 -22
  96. cfclient/ui/widgets/super_slider.py +112 -0
  97. cfclient/ui/wizards/__init__.py +0 -0
  98. cfclient/ui/wizards/bslh_1.png +0 -0
  99. cfclient/ui/wizards/bslh_2.png +0 -0
  100. cfclient/ui/wizards/bslh_3.png +0 -0
  101. cfclient/ui/wizards/bslh_4.png +0 -0
  102. cfclient/ui/wizards/bslh_5.png +0 -0
  103. cfclient/ui/wizards/lighthouse_geo_bs_estimation_wizard.py +465 -0
  104. cfclient/utils/config_manager.py +5 -4
  105. cfclient/utils/input/__init__.py +77 -19
  106. cfclient/utils/input/inputinterfaces/wiimote.py +2 -2
  107. cfclient/utils/input/inputreaderinterface.py +17 -7
  108. cfclient/utils/input/inputreaders/__init__.py +17 -0
  109. cfclient/utils/logconfigreader.py +245 -25
  110. cfclient/utils/logdatawriter.py +3 -1
  111. cfclient/utils/periodictimer.py +1 -1
  112. cfclient/utils/ui.py +336 -0
  113. cfclient/utils/zmq_led_driver.py +5 -0
  114. cfclient/utils/zmq_param.py +6 -0
  115. cfclient/version.py +34 -1
  116. cfclient-2025.12.1.dist-info/METADATA +70 -0
  117. cfclient-2025.12.1.dist-info/RECORD +152 -0
  118. {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/WHEEL +1 -1
  119. {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/entry_points.txt +0 -1
  120. cfclient-2025.12.1.dist-info/licenses/LICENSE.txt +350 -0
  121. {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/top_level.txt +1 -0
  122. cfconfig/Makefile +51 -0
  123. cfconfig/configblock.py +111 -0
  124. cfloader/__init__.py +41 -55
  125. cfzmq/__init__.py +22 -14
  126. cfclient/ui/dialogs/cf1config.py +0 -265
  127. cfclient/ui/dialogs/cf1config.ui +0 -260
  128. cfclient/ui/tab.py +0 -96
  129. cfclient/ui/tabs/LEDTab.py +0 -169
  130. cfclient/ui/toolboxes/ConsoleToolbox.py +0 -69
  131. cfclient/ui/toolboxes/DebugDriverToolbox.py +0 -107
  132. cfclient/ui/toolboxes/__init__.py +0 -45
  133. cfclient/ui/toolboxes/consoleToolbox.ui +0 -62
  134. cfclient/ui/toolboxes/debugDriverToolbox.ui +0 -86
  135. cfclient-2017.4.dist-info/DESCRIPTION.rst +0 -3
  136. cfclient-2017.4.dist-info/METADATA +0 -22
  137. cfclient-2017.4.dist-info/RECORD +0 -104
  138. cfclient-2017.4.dist-info/metadata.json +0 -1
  139. /cfclient/{icon-256.png → ui/icons/icon-256.png} +0 -0
  140. /cfclient/ui/{toolboxes → tabs}/crtpSharkToolbox.ui +0 -0
@@ -7,7 +7,7 @@
7
7
  # +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
8
8
  # || || /_____/_/\__/\___/_/ \__,_/ /___/\___/
9
9
  #
10
- # Copyright (C) 2011-2017 Bitcraze AB
10
+ # Copyright (C) 2011-2023 Bitcraze AB
11
11
  #
12
12
  # Crazyflie Nano Quadcopter Client
13
13
  #
@@ -34,43 +34,36 @@ import logging
34
34
  from enum import Enum
35
35
  from collections import namedtuple
36
36
 
37
- from PyQt5 import uic
38
- from PyQt5.QtCore import pyqtSignal, QTimer
39
- from PyQt5.QtGui import QFont
40
- from PyQt5.QtGui import QMessageBox
37
+ import time
38
+ from PyQt6 import uic
39
+ from PyQt6.QtCore import Qt, pyqtSignal, QTimer
40
+ from PyQt6.QtWidgets import QMessageBox
41
+ from PyQt6.QtWidgets import QLabel
41
42
 
42
43
  import cfclient
43
- from cfclient.ui.tab import Tab
44
+ from cfclient.ui.tab_toolbox import TabToolbox
44
45
 
45
46
  from cflib.crazyflie.log import LogConfig
46
47
  from cflib.crazyflie.mem import MemoryElement
47
48
  from lpslib.lopoanchor import LoPoAnchor
48
49
 
50
+ from cfclient.ui.dialogs.anchor_position_dialog import AnchorPositionDialog
51
+
52
+ from vispy import scene
53
+ import numpy as np
54
+
49
55
  import copy
50
- import sys
51
56
 
52
57
  __author__ = 'Bitcraze AB'
53
58
  __all__ = ['LocoPositioningTab']
54
59
 
55
60
  logger = logging.getLogger(__name__)
56
61
 
57
- locopositioning_tab_class = uic.loadUiType(
58
- cfclient.module_path + "/ui/tabs/locopositioning_tab.ui")[0]
59
-
60
- # Try the imports for PyQtGraph to see if it is installed
61
- try:
62
- import pyqtgraph as pg
63
- from pyqtgraph import ViewBox # noqa
64
- import pyqtgraph.console # noqa
65
- import numpy as np # noqa
66
-
67
- _pyqtgraph_found = True
68
- except Exception:
69
- import traceback
62
+ locopositioning_tab_class = uic.loadUiType(cfclient.module_path + "/ui/tabs/locopositioning_tab.ui")[0]
70
63
 
71
- logger.warning("PyQtGraph (or dependency) failed to import:\n%s",
72
- traceback.format_exc())
73
- _pyqtgraph_found = False
64
+ STYLE_RED_BACKGROUND = "background-color: lightpink;"
65
+ STYLE_GREEN_BACKGROUND = "background-color: lightgreen;"
66
+ STYLE_NO_BACKGROUND = "background-color: none;"
74
67
 
75
68
 
76
69
  class Anchor:
@@ -78,6 +71,8 @@ class Anchor:
78
71
  self.x = x
79
72
  self.y = y
80
73
  self.z = z
74
+ self._is_position_valid = False
75
+ self._is_active = False
81
76
  self.distance = distance
82
77
 
83
78
  def set_position(self, position):
@@ -85,11 +80,21 @@ class Anchor:
85
80
  self.x = position[0]
86
81
  self.y = position[1]
87
82
  self.z = position[2]
83
+ self._is_position_valid = True
88
84
 
89
85
  def get_position(self):
90
86
  """Returns the position as a vector"""
91
87
  return (self.x, self.y, self.z)
92
88
 
89
+ def is_position_valid(self):
90
+ return self._is_position_valid
91
+
92
+ def set_is_active(self, is_active):
93
+ self._is_active = is_active
94
+
95
+ def is_active(self):
96
+ return self._is_active
97
+
93
98
 
94
99
  class AxisScaleStep:
95
100
  def __init__(self, from_view, from_axis, to_view, to_axis,
@@ -101,136 +106,158 @@ class AxisScaleStep:
101
106
  self.center_only = center_only
102
107
 
103
108
 
104
- class PlotWrapper:
105
- XAxis = 0
106
- YAxis = 1
107
- _refs = []
108
- _change_lock = False
109
-
110
- axis_dict = {'x': 0, 'y': 1, 'z': 2}
111
-
112
- ANCHOR_BRUSH = (60, 60, 60)
113
- HIGHLIGHT_ANCHOR_BRUSH = (0, 255, 0)
114
- POSITION_BRUSH = (0, 0, 255)
109
+ class Plot3dLps(scene.SceneCanvas):
110
+ ANCHOR_BRUSH = np.array((0.2, 0.5, 0.2))
111
+ ANCHOR_BRUSH_INVALID = np.array((0.8, 0.5, 0.5))
112
+ HIGHLIGHT_ANCHOR_BRUSH = np.array((0, 1, 0))
113
+ POSITION_BRUSH = np.array((0, 0, 1.0))
115
114
 
116
115
  VICINITY_DISTANCE = 2.5
117
116
  HIGHLIGHT_DISTANCE = 0.5
118
117
 
119
- LABEL_SIZE = 15
120
- LABEL_HIGHLIGHT_SIZE = 30
118
+ LABEL_SIZE = 100
119
+ LABEL_HIGHLIGHT_SIZE = 200
121
120
 
122
121
  ANCHOR_SIZE = 10
123
122
  HIGHLIGHT_SIZE = 20
124
123
 
125
- def __init__(self, title, horizontal, vertical):
126
- self._horizontal = horizontal
127
- self._vertical = vertical
128
- self._depth = self._find_missing_axis(horizontal, vertical)
129
- self._title = title
130
- self.widget = pg.PlotWidget(title=title, enableMenu=False)
131
- self.widget.getPlotItem().hideButtons()
132
- self._axis_scale_steps = []
133
-
134
- self.widget.setLabel('left', self._vertical, units='m')
135
- self.widget.setLabel('bottom', self._horizontal, units='m')
136
-
137
- self.widget.setAspectLocked(True, 1)
138
- self.widget.getViewBox().sigRangeChanged.connect(self._view_changed)
139
- self.view = self.widget.getViewBox()
140
-
141
- def update(self, anchors, pos, display_mode):
142
- self.widget.clear()
143
-
144
- # Sort anchors in depth order to add the one closes last
145
- for (i, anchor) in sorted(
146
- anchors.items(),
147
- key=lambda item: getattr(item[1], self._depth), reverse=True):
148
- anchor_v = getattr(anchor, self._horizontal)
149
- anchor_h = getattr(anchor, self._vertical)
150
- self._plot_anchor(anchor_v, anchor_h, i, anchor.distance,
151
- display_mode)
152
-
153
- if display_mode is DisplayMode.estimated_position:
154
- cf_h = pos[PlotWrapper.axis_dict[self._horizontal]]
155
- cf_v = pos[PlotWrapper.axis_dict[self._vertical]]
156
- self.widget.plot([cf_h], [cf_v], pen=None,
157
- symbolBrush=PlotWrapper.POSITION_BRUSH)
158
-
159
- def _find_missing_axis(self, axis1, axis2):
160
- all = set(self.axis_dict.keys())
161
- all.remove(axis1)
162
- all.remove(axis2)
163
-
164
- return list(all)[0]
165
-
166
- def _plot_anchor(self, x, y, anchor_id, distance, display_mode):
167
- brush = PlotWrapper.ANCHOR_BRUSH
168
- size = PlotWrapper.ANCHOR_SIZE
124
+ TEXT_OFFSET = np.array((0.0, 0, 0.25))
125
+
126
+ def __init__(self):
127
+ scene.SceneCanvas.__init__(self, keys=None)
128
+ self.unfreeze()
129
+
130
+ self._view = self.central_widget.add_view()
131
+ self._view.bgcolor = '#ffffff'
132
+ self._view.camera = scene.TurntableCamera(
133
+ distance=10.0,
134
+ up='+z',
135
+ center=(0.0, 0.0, 1.0))
136
+
137
+ self._cf = scene.visuals.Markers(
138
+ pos=np.array([[0, 0, 0]]),
139
+ parent=self._view.scene,
140
+ face_color=self.POSITION_BRUSH)
141
+ self._anchor_contexts = {}
142
+
143
+ self.freeze()
144
+
145
+ plane_size = 10
146
+ scene.visuals.Plane(
147
+ width=plane_size,
148
+ height=plane_size,
149
+ width_segments=plane_size,
150
+ height_segments=plane_size,
151
+ color=(0.5, 0.5, 0.5, 0.5),
152
+ edge_color="gray",
153
+ parent=self._view.scene)
154
+
155
+ self.addArrows(1, 0.02, 0.1, 0.1, self._view.scene)
156
+
157
+ def addArrows(self, length, width, head_length, head_width, parent):
158
+ # The Arrow visual in vispy does not seem to work very good,
159
+ # draw arrows using lines instead.
160
+ w = width / 2
161
+ hw = head_width / 2
162
+ base_len = length - head_length
163
+
164
+ # X-axis
165
+ scene.visuals.LinePlot([
166
+ [0, w, 0],
167
+ [base_len, w, 0],
168
+ [base_len, hw, 0],
169
+ [length, 0, 0],
170
+ [base_len, -hw, 0],
171
+ [base_len, -w, 0],
172
+ [0, -w, 0]],
173
+ width=1.0, color='red', parent=parent, marker_size=0.0)
174
+
175
+ # Y-axis
176
+ scene.visuals.LinePlot([
177
+ [w, 0, 0],
178
+ [w, base_len, 0],
179
+ [hw, base_len, 0],
180
+ [0, length, 0],
181
+ [-hw, base_len, 0],
182
+ [-w, base_len, 0],
183
+ [-w, 0, 0]],
184
+ width=1.0, color='green', parent=parent, marker_size=0.0)
185
+
186
+ # Z-axis
187
+ scene.visuals.LinePlot([
188
+ [0, w, 0],
189
+ [0, w, base_len],
190
+ [0, hw, base_len],
191
+ [0, 0, length],
192
+ [0, -hw, base_len],
193
+ [0, -w, base_len],
194
+ [0, -w, 0]],
195
+ width=1.0, color='blue', parent=parent, marker_size=0.0)
196
+
197
+ def update_data(self, anchors, pos, display_mode):
198
+ self._cf.set_data(pos=np.array([pos]), face_color=self.POSITION_BRUSH)
199
+
200
+ for id, anchor in anchors.items():
201
+ self._update_anchor(id, anchor, display_mode)
202
+
203
+ self._purge_anchors(anchors.keys())
204
+
205
+ def _update_anchor(self, id, anchor, display_mode):
206
+ if anchor.is_active():
207
+ color = self.ANCHOR_BRUSH
208
+ else:
209
+ color = self.ANCHOR_BRUSH_INVALID
210
+
211
+ size = self.ANCHOR_SIZE
169
212
  font_size = self.LABEL_SIZE
213
+ distance = anchor.distance
170
214
  if display_mode is DisplayMode.identify_anchor:
171
- if distance < PlotWrapper.VICINITY_DISTANCE:
172
- brush = self._mix_brushes(
173
- brush,
174
- PlotWrapper.HIGHLIGHT_ANCHOR_BRUSH,
175
- distance / PlotWrapper.VICINITY_DISTANCE)
176
-
177
- if distance < PlotWrapper.HIGHLIGHT_DISTANCE:
178
- brush = PlotWrapper.HIGHLIGHT_ANCHOR_BRUSH
179
- size = PlotWrapper.HIGHLIGHT_SIZE
215
+ if distance < self.VICINITY_DISTANCE:
216
+ amount = (distance - self.HIGHLIGHT_DISTANCE) / \
217
+ (self.VICINITY_DISTANCE - self.HIGHLIGHT_DISTANCE)
218
+ color = self._mix(color, self.HIGHLIGHT_ANCHOR_BRUSH, amount)
219
+
220
+ if distance < self.HIGHLIGHT_DISTANCE:
221
+ color = self.HIGHLIGHT_ANCHOR_BRUSH
222
+ size = self.HIGHLIGHT_SIZE
180
223
  font_size = self.LABEL_HIGHLIGHT_SIZE
181
224
 
182
- self.widget.plot([x], [y], pen=None, symbolBrush=brush,
183
- symbolSize=size)
184
-
185
- text = pg.TextItem(text="{}".format(anchor_id))
186
- font = QFont("Helvetica", font_size)
187
- text.setFont(font)
188
- self.widget.addItem(text)
189
- text.setPos(x, y)
190
-
191
- def _mix_brushes(self, brush1, brush2, mix):
192
- if mix < 0.0:
193
- return brush1
194
- if mix > 1.0:
195
- return brush2
196
-
197
- b1 = mix
198
- b2 = 1.0 - mix
199
- return (
200
- brush1[0] * b1 + brush2[0] * b2,
201
- brush1[1] * b1 + brush2[1] * b2,
202
- brush1[2] * b1 + brush2[2] * b2,
203
- )
204
-
205
- def _view_changed(self, view, settings):
206
- # Ignore all callbacks until this change is processed
207
- if PlotWrapper._change_lock:
208
- return
209
- PlotWrapper._change_lock = True
210
-
211
- for step in self._axis_scale_steps:
212
- range = step.from_view.viewRange()[step.from_axis]
213
- new_range = range
214
-
215
- if step.center_only:
216
- center = (range[0] + range[1]) / 2
217
- current_range = step.to_view.viewRange()[step.to_axis]
218
- current_center = (current_range[0] + current_range[1]) / 2
219
- delta = center - current_center
220
- new_range = [current_range[0] + delta,
221
- current_range[1] + delta]
222
-
223
- if step.to_axis is PlotWrapper.XAxis:
224
- step.to_view.setRange(xRange=new_range, padding=0.0,
225
- update=True)
226
- else:
227
- step.to_view.setRange(yRange=new_range, padding=0.0,
228
- update=True)
229
-
230
- PlotWrapper._change_lock = False
231
-
232
- def set_scale_steps(self, steps):
233
- self._axis_scale_steps = steps
225
+ marker_pos = anchor.get_position()
226
+ text_pos = self.TEXT_OFFSET + marker_pos
227
+ if id in self._anchor_contexts:
228
+ self._anchor_contexts[id][0].set_data(
229
+ pos=np.array([marker_pos]),
230
+ face_color=color,
231
+ size=size)
232
+
233
+ text = self._anchor_contexts[id][1]
234
+ text.pos = text_pos
235
+ text.font_size = font_size
236
+ else:
237
+ marker = scene.visuals.Markers(
238
+ pos=np.array([marker_pos]),
239
+ face_color=color,
240
+ size=size,
241
+ parent=self._view.scene)
242
+ text = scene.visuals.Text(
243
+ text=str(id),
244
+ font_size=font_size,
245
+ pos=text_pos,
246
+ parent=self._view.scene)
247
+ self._anchor_contexts[id] = [marker, text]
248
+
249
+ def _purge_anchors(self, keep):
250
+ to_remove = []
251
+ for id, context in self._anchor_contexts.items():
252
+ if id not in keep:
253
+ to_remove.append(id)
254
+ for visual in context:
255
+ visual.parent = None
256
+ for id in to_remove:
257
+ self._anchor_contexts.pop(id)
258
+
259
+ def _mix(self, col1, col2, mix):
260
+ return col1 * mix + col2 * (1.0 - mix)
234
261
 
235
262
 
236
263
  class DisplayMode(Enum):
@@ -241,38 +268,99 @@ class DisplayMode(Enum):
241
268
  Range = namedtuple('Range', ['min', 'max'])
242
269
 
243
270
 
244
- class AnchorPosWrapper():
245
- """Wraps the UI elements of one anchor position"""
246
- def __init__(self, x, y, z):
247
- self._x = x
248
- self._y = y
249
- self._z = z
271
+ class AnchorStateMachine:
272
+ GET_ACTIVE = 0
273
+ GET_IDS = 1
274
+ GET_DATA = 2
275
+ STEPS = [
276
+ GET_ACTIVE,
277
+ GET_ACTIVE,
278
+ GET_IDS,
279
+ GET_ACTIVE,
280
+ GET_ACTIVE,
281
+ GET_DATA,
282
+ GET_ACTIVE,
283
+ GET_ACTIVE,
284
+ ]
285
+
286
+ def __init__(self, mem_sub, cb_active_id_list, cb_id_list, cb_data):
287
+ self._current_step = 0
288
+ self._waiting_for_response = False
289
+ self._mem = self._get_mem(mem_sub)
290
+
291
+ self._cb_active_id_list = cb_active_id_list
292
+ self._cb_id_list = cb_id_list
293
+ self._cb_data = cb_data
294
+
295
+ def poll(self):
296
+ if not self._waiting_for_response:
297
+ self._next_step()
298
+ self._waiting_for_response = self._request_step()
299
+
300
+ def _next_step(self):
301
+ self._current_step += 1
302
+ if self._current_step >= len(AnchorStateMachine.STEPS):
303
+ self._current_step = 0
304
+
305
+ def _request_step(self):
306
+ result = True
307
+
308
+ action = AnchorStateMachine.STEPS[self._current_step]
309
+ if action == AnchorStateMachine.GET_ACTIVE:
310
+ self._mem.update_active_id_list(self._cb_active_id_list_updated)
311
+ elif action == AnchorStateMachine.GET_IDS:
312
+ self._mem.update_id_list(self._cb_id_list_updated)
313
+ else:
314
+ if self._mem.nr_of_anchors > 0:
315
+ # Only request anchor data if we actually have anchors, otherwise the callback will never be called
316
+ self._mem.update_data(self._cb_data_updated)
317
+ else:
318
+ result = False
250
319
 
251
- def get_position(self):
252
- """Get the position from the UI elements"""
253
- return (self._x.value(), self._y.value(), self._z.value())
320
+ return result
254
321
 
255
- def set_position(self, position):
256
- """Set the position in the UI elements"""
257
- self._x.setValue(position[0])
258
- self._y.setValue(position[1])
259
- self._z.setValue(position[2])
322
+ def _get_mem(self, mem_sub):
323
+ mem = mem_sub.get_mems(MemoryElement.TYPE_LOCO2)
324
+ if len(mem) > 0:
325
+ return mem[0]
326
+ return None
327
+
328
+ def _cb_active_id_list_updated(self, mem_data):
329
+ self._waiting_for_response = False
330
+ if self._cb_active_id_list:
331
+ self._cb_active_id_list(mem_data.active_anchor_ids)
260
332
 
261
- def enable(self, enabled):
262
- """Enable/disable all UI elements for the position"""
263
- self._x.setEnabled(enabled)
264
- self._y.setEnabled(enabled)
265
- self._z.setEnabled(enabled)
333
+ def _cb_id_list_updated(self, mem_data):
334
+ self._waiting_for_response = False
335
+ if self._cb_id_list:
336
+ self._cb_id_list(mem_data.anchor_ids)
266
337
 
338
+ def _cb_data_updated(self, mem_data):
339
+ self._waiting_for_response = False
340
+ if self._cb_data:
341
+ self._cb_data(mem_data.anchor_data)
267
342
 
268
- class LocoPositioningTab(Tab, locopositioning_tab_class):
343
+
344
+ class LocoPositioningTab(TabToolbox, locopositioning_tab_class):
269
345
  """Tab for plotting Loco Positioning data"""
270
346
 
271
347
  # Update period of log data in ms
272
348
  UPDATE_PERIOD_LOG = 100
273
349
 
274
350
  # Update period of anchor position data
275
- UPDATE_PERIOD_ANCHOR_POS = 5000
351
+ UPDATE_PERIOD_ANCHOR_STATE = 1000
352
+
353
+ UPDATE_PERIOD_LOCO_MODE = 1000
354
+
355
+ LOCO_MODE_UNKNOWN = -1
356
+ LOCO_MODE_AUTO = 0
357
+ LOCO_MODE_TWR = 1
358
+ LOCO_MODE_TDOA2 = 2
359
+ LOCO_MODE_TDOA3 = 3
360
+
361
+ PARAM_MDOE_GR = 'loco'
362
+ PARAM_MODE_NM = 'mode'
363
+ PARAM_MODE = PARAM_MDOE_GR + '.' + PARAM_MODE_NM
276
364
 
277
365
  # Frame rate (updates per second)
278
366
  FPS = 2
@@ -281,21 +369,17 @@ class LocoPositioningTab(Tab, locopositioning_tab_class):
281
369
  _disconnected_signal = pyqtSignal(str)
282
370
  _log_error_signal = pyqtSignal(object, str)
283
371
  _anchor_range_signal = pyqtSignal(int, object, object)
284
- _position_signal = pyqtSignal(int, object, object)
285
- _anchor_position_signal = pyqtSignal(object)
286
-
287
- def __init__(self, tabWidget, helper, *args):
288
- super(LocoPositioningTab, self).__init__(*args)
289
- self.setupUi(self)
372
+ _loco_sys_signal = pyqtSignal(int, object, object)
373
+ _cb_param_to_detect_loco_deck_signal = pyqtSignal(object, object)
290
374
 
291
- self.tabName = "Loco Positioning"
292
- self.menuName = "Loco Positioning Tab"
293
- self.tabWidget = tabWidget
375
+ _anchor_active_id_list_updated_signal = pyqtSignal(object)
376
+ _anchor_data_updated_signal = pyqtSignal(object)
294
377
 
295
- self._helper = helper
378
+ def __init__(self, helper):
379
+ super(LocoPositioningTab, self).__init__(helper, 'Loco Positioning')
380
+ self.setupUi(self)
296
381
 
297
382
  self._anchors = {}
298
- self._position = []
299
383
  self._clear_state()
300
384
  self._refs = []
301
385
 
@@ -306,34 +390,70 @@ class LocoPositioningTab(Tab, locopositioning_tab_class):
306
390
  self._connected_signal.connect(self._connected)
307
391
  self._disconnected_signal.connect(self._disconnected)
308
392
  self._anchor_range_signal.connect(self._anchor_range_received)
309
- self._position_signal.connect(self._position_received)
310
- self._anchor_position_signal.connect(self._anchor_positions_updated)
393
+ self._loco_sys_signal.connect(self._loco_sys_received)
394
+ self._cb_param_to_detect_loco_deck_signal.connect(
395
+ self._cb_param_to_detect_loco_deck)
396
+
397
+ self._anchor_active_id_list_updated_signal.connect(
398
+ self._active_id_list_updated)
399
+ self._anchor_data_updated_signal.connect(
400
+ self._anchor_data_updated)
311
401
 
312
- self._id_anchor_button.clicked.connect(
402
+ self._id_anchor_button.toggled.connect(
313
403
  lambda enabled:
314
- self._set_display_mode(DisplayMode.identify_anchor)
404
+ self._do_when_checked(
405
+ enabled,
406
+ self._set_display_mode,
407
+ DisplayMode.identify_anchor)
315
408
  )
316
409
 
317
- self._estimated_postion_button.clicked.connect(
410
+ self._estimated_postion_button.toggled.connect(
318
411
  lambda enabled:
319
- self._set_display_mode(DisplayMode.estimated_position)
412
+ self._do_when_checked(
413
+ enabled,
414
+ self._set_display_mode,
415
+ DisplayMode.estimated_position)
320
416
  )
321
417
 
322
- self._anchor_pos_ui = {}
323
- for anchor_nr in range(0, 8):
324
- self._register_anchor_pos_ui(anchor_nr)
418
+ self._mode_auto.toggled.connect(
419
+ lambda enabled: self._request_mode(enabled, self.LOCO_MODE_AUTO)
420
+ )
325
421
 
326
- self._write_pos_to_anhors_button.clicked.connect(
327
- lambda enabled:
328
- self._write_positions_to_anchors()
422
+ self._mode_twr.toggled.connect(
423
+ lambda enabled: self._request_mode(enabled, self.LOCO_MODE_TWR)
329
424
  )
330
425
 
331
- self._read_pos_from_anhors_button.clicked.connect(
426
+ self._mode_tdoa2.toggled.connect(
427
+ lambda enabled: self._request_mode(enabled, self.LOCO_MODE_TDOA2)
428
+ )
429
+
430
+ self._mode_tdoa3.toggled.connect(
431
+ lambda enabled: self._request_mode(enabled, self.LOCO_MODE_TDOA3)
432
+ )
433
+
434
+ self._enable_mode_buttons(False)
435
+
436
+ self._switch_mode_to_twr_button.setEnabled(False)
437
+ self._switch_mode_to_tdoa2_button.setEnabled(False)
438
+ self._switch_mode_to_tdoa3_button.setEnabled(False)
439
+
440
+ self._switch_mode_to_twr_button.clicked.connect(
332
441
  lambda enabled:
333
- self._read_positions_from_anchors()
442
+ self._send_anchor_mode(self.LOCO_MODE_TWR)
334
443
  )
444
+ self._switch_mode_to_tdoa2_button.clicked.connect(
445
+ lambda enabled:
446
+ self._send_anchor_mode(self.LOCO_MODE_TDOA2)
447
+ )
448
+ self._switch_mode_to_tdoa3_button.clicked.connect(
449
+ lambda enabled:
450
+ self._send_anchor_mode(self.LOCO_MODE_TDOA3)
451
+ )
452
+
453
+ self._clear_anchors_button.clicked.connect(self._clear_anchors)
335
454
 
336
- self._show_all_button.clicked.connect(self._scale_and_center_graphs)
455
+ self._configure_anchor_positions_button.clicked.connect(
456
+ self._show_anchor_postion_dialog)
337
457
 
338
458
  # Connect the Crazyflie API callbacks to the signals
339
459
  self._helper.cf.connected.add_callback(
@@ -344,193 +464,96 @@ class LocoPositioningTab(Tab, locopositioning_tab_class):
344
464
 
345
465
  self._set_up_plots()
346
466
 
467
+ self.is_loco_deck_active = False
468
+
347
469
  self._graph_timer = QTimer()
348
- self._graph_timer.setInterval(1000 / self.FPS)
470
+ self._graph_timer.setInterval(int(1000 / self.FPS))
349
471
  self._graph_timer.timeout.connect(self._update_graphics)
350
472
  self._graph_timer.start()
351
473
 
352
- self._anchor_pos_timer = QTimer()
353
- self._anchor_pos_timer.setInterval(self.UPDATE_PERIOD_ANCHOR_POS)
354
- self._anchor_pos_timer.timeout.connect(self._poll_anchor_positions)
355
-
356
- def _register_anchor_pos_ui(self, nr):
357
- x_spin = getattr(self, 'spin_a{}x'.format(nr))
358
- y_spin = getattr(self, 'spin_a{}y'.format(nr))
359
- z_spin = getattr(self, 'spin_a{}z'.format(nr))
360
- self._anchor_pos_ui[nr] = AnchorPosWrapper(x_spin, y_spin, z_spin)
361
-
362
- def _write_positions_to_anchors(self):
363
- lopo = LoPoAnchor(self._helper.cf)
474
+ self._anchor_state_timer = QTimer()
475
+ self._anchor_state_timer.setInterval(self.UPDATE_PERIOD_ANCHOR_STATE)
476
+ self._anchor_state_timer.timeout.connect(self._poll_anchor_state)
477
+ self._anchor_state_machine = None
364
478
 
365
- for id, anchor_pos in self._anchor_pos_ui.items():
366
- if id in self._anchors:
367
- position = anchor_pos.get_position()
368
- lopo.set_position(id, position)
479
+ self._update_position_label(self._helper.pose_logger.position)
369
480
 
370
- def _read_positions_from_anchors(self):
371
- for id, anchor_pos in self._anchor_pos_ui.items():
372
- position = (0.0, 0.0, 0.0)
373
- if id in self._anchors:
374
- position = self._anchors[id].get_position()
481
+ self._lps_state = self.LOCO_MODE_UNKNOWN
482
+ self._update_lps_state(self.LOCO_MODE_UNKNOWN)
375
483
 
376
- anchor_pos.set_position(position)
484
+ self._anchor_position_dialog = AnchorPositionDialog(self, helper)
485
+ self._configure_anchor_positions_button.setEnabled(False)
377
486
 
378
- def _enable_anchor_pos_ui(self):
379
- for id, anchor_pos in self._anchor_pos_ui.items():
380
- exists = id in self._anchors
381
- anchor_pos.enable(exists)
487
+ def _do_when_checked(self, enabled, fkn, arg):
488
+ if enabled:
489
+ fkn(arg)
382
490
 
383
491
  def _set_up_plots(self):
384
- self._plot_xy = PlotWrapper("Top view (X/Y)", "x", "y")
385
- self._plot_top_left_layout.addWidget(self._plot_xy.widget)
386
- self._plot_xz = PlotWrapper("Front view (X/Z)", "x", "z")
387
- self._plot_bottom_left_layout.addWidget(self._plot_xz.widget)
388
- self._plot_yz = PlotWrapper("Right view (Y/Z)", "y", "z")
389
- self._plot_bottom_right_layout.addWidget(self._plot_yz.widget)
390
- self._plot_xy.set_scale_steps([
391
- AxisScaleStep(self._plot_xy, PlotWrapper.XAxis,
392
- self._plot_xz, PlotWrapper.XAxis),
393
- AxisScaleStep(self._plot_xz, PlotWrapper.YAxis,
394
- self._plot_yz, PlotWrapper.YAxis),
395
- AxisScaleStep(self._plot_xy, PlotWrapper.YAxis,
396
- self._plot_yz, PlotWrapper.XAxis, center_only=True)
397
- ])
398
- self._plot_xz.set_scale_steps([
399
- AxisScaleStep(self._plot_xz, PlotWrapper.XAxis,
400
- self._plot_xy, PlotWrapper.XAxis),
401
- AxisScaleStep(self._plot_xz, PlotWrapper.YAxis,
402
- self._plot_yz, PlotWrapper.YAxis),
403
- AxisScaleStep(self._plot_xy, PlotWrapper.YAxis,
404
- self._plot_yz, PlotWrapper.XAxis, center_only=True)
405
- ])
406
- self._plot_yz.set_scale_steps([
407
- AxisScaleStep(self._plot_yz, PlotWrapper.YAxis,
408
- self._plot_xz, PlotWrapper.YAxis),
409
- AxisScaleStep(self._plot_xz, PlotWrapper.XAxis,
410
- self._plot_xy, PlotWrapper.XAxis),
411
- AxisScaleStep(self._plot_yz, PlotWrapper.XAxis,
412
- self._plot_xy, PlotWrapper.YAxis, center_only=True)
413
- ])
414
-
415
- self._plot_xy.view.setRange(xRange=(0.0, 5.0))
492
+ self._plot_3d = Plot3dLps()
493
+ self._plot_layout.addWidget(self._plot_3d.native)
416
494
 
417
495
  def _set_display_mode(self, display_mode):
418
496
  self._display_mode = display_mode
419
497
 
420
- def _clear_state(self):
421
- self._anchors = {}
422
- self._position = [0.0, 0.0, 0.0]
423
-
424
- def _scale_and_center_graphs(self):
425
- start_bounds = Range(sys.float_info.max, -sys.float_info.max)
426
- bounds = {"x": start_bounds, "y": start_bounds,
427
- "z": start_bounds}
428
- for a in self._anchors.values():
429
- bounds = self._find_min_max_data_range(bounds, [a.x, a.y, a.z])
430
- bounds = self._find_min_max_data_range(bounds, self._position)
431
-
432
- bounds = self._pad_bounds(bounds)
433
- self._center_all_data_in_graphs(bounds)
434
- self._rescale_to_fit_data(bounds)
435
-
436
- def _rescale_to_fit_data(self, bounds):
437
- [[xy_xmin, xy_xmax],
438
- [xy_ymin, xy_ymax]] = self._plot_xy.view.viewRange()
439
- [[yz_xmin, yz_xmax],
440
- [yz_ymin, yz_ymax]] = self._plot_yz.view.viewRange()
441
- if not self._is_data_visibile(bounds, self._position):
442
- if self._will_new_range_zoom_in(Range(xy_xmin, xy_xmax),
443
- bounds["x"]):
444
- self._plot_xy.view.setRange(xRange=bounds["x"],
445
- padding=0.0, update=True)
446
-
447
- if not self._is_data_visibile(bounds, self._position):
448
- if self._will_new_range_zoom_in(Range(xy_ymin, xy_ymax),
449
- bounds["y"]):
450
- self._plot_xy.view.setRange(yRange=bounds["y"],
451
- padding=0.0, update=True)
452
-
453
- if not self._is_data_visibile(bounds, self._position):
454
- if self._will_new_range_zoom_in(Range(yz_xmin, yz_xmax),
455
- bounds["y"]):
456
- self._plot_yz.view.setRange(yRange=bounds["y"],
457
- padding=0.0, update=True)
458
-
459
- if not self._is_data_visibile(bounds, self._position):
460
- if self._will_new_range_zoom_in(Range(yz_ymin, yz_ymax),
461
- bounds["z"]):
462
- self._plot_yz.view.setRange(yRange=bounds["z"], padding=0.0,
463
- update=True)
464
-
465
- def _pad_bounds(self, ranges):
466
- new_ranges = ranges
467
-
468
- new_ranges["x"] = Range(new_ranges["x"].min - 1.0,
469
- new_ranges["x"].max + 1.0)
470
-
471
- new_ranges["y"] = Range(new_ranges["y"].min - 1.0,
472
- new_ranges["y"].max + 1.0)
473
-
474
- new_ranges["z"] = Range(new_ranges["z"].min - 1.0,
475
- new_ranges["z"].max + 1.0)
476
-
477
- return new_ranges
478
-
479
- def _center_all_data_in_graphs(self, ranges):
480
- # Will center data in graphs without taking care of scaling
481
- self._plot_xy.view.setRange(xRange=ranges["x"], yRange=ranges["y"],
482
- padding=0.0, update=True)
483
- self._plot_yz.view.setRange(yRange=ranges["z"], padding=0.0,
484
- update=True)
485
-
486
- def _will_new_range_zoom_in(self, old_range, new_range):
487
- return old_range.min > new_range.min
488
-
489
- def _is_data_visibile(self, ranges, point):
490
- [[xy_xmin, xy_xmax],
491
- [xy_ymin, xy_ymax]] = self._plot_xy.view.viewRange()
492
- [[yz_xmin, yz_xmax],
493
- [yz_ymin, yz_ymax]] = self._plot_yz.view.viewRange()
494
- [[xz_xmin, xz_xmax],
495
- [xz_ymin, xz_ymax]] = self._plot_xz.view.viewRange()
496
-
497
- allVisible = True
498
-
499
- if ranges["x"].min < xy_xmin or ranges["x"].max > xy_xmax:
500
- allVisible = False
501
-
502
- if ranges["z"].min < yz_ymin or ranges["z"].max > yz_ymax:
503
- allVisible = False
504
-
505
- if ranges["y"].min < yz_xmin or ranges["y"].max > yz_xmax:
506
- allVisible = False
507
-
508
- if ranges["y"].min < xy_ymin or ranges["y"].max > xy_ymax:
509
- allVisible = False
510
-
511
- return allVisible
512
-
513
- def _find_min_max_data_range(self, ranges, point):
514
- result = ranges
498
+ def _send_anchor_mode(self, mode):
499
+ lopo = LoPoAnchor(self._helper.cf)
515
500
 
516
- result["x"] = Range(min(ranges["x"].min, point[0]),
517
- max(ranges["x"].max, point[0]))
501
+ mode_translation = {
502
+ self.LOCO_MODE_TWR: lopo.MODE_TWR,
503
+ self.LOCO_MODE_TDOA2: lopo.MODE_TDOA,
504
+ self.LOCO_MODE_TDOA3: lopo.MODE_TDOA3,
505
+ }
518
506
 
519
- result["y"] = Range(min(ranges["y"].min, point[1]),
520
- max(ranges["y"].max, point[1]))
507
+ # Set the mode from the last to the first anchor
508
+ # In TDoA 2 mode this ensures that the master anchor is set last
509
+ # Note: We only switch mode of anchor 0 - 7 since this is what is
510
+ # supported in TWR and TDoA 2
511
+ for j in range(5):
512
+ for i in reversed(range(8)):
513
+ lopo.set_mode(i, mode_translation[mode])
521
514
 
522
- result["z"] = Range(min(ranges["z"].min, point[2]),
523
- max(ranges["z"].max, point[2]))
515
+ def _clear_state(self):
516
+ self._clear_anchors()
517
+ self._update_ranging_status_indicators()
518
+ self._id_anchor_button.setEnabled(True)
524
519
 
525
- return result
520
+ def _clear_anchors(self):
521
+ self._anchors = {}
526
522
 
527
523
  def _connected(self, link_uri):
528
524
  """Callback when the Crazyflie has been connected"""
529
525
  logger.debug("Crazyflie connected to {}".format(link_uri))
530
-
531
- self._clear_state()
532
-
533
- if self._helper.cf.mem.ow_search(vid=0xBC, pid=0x06):
526
+ self._request_param_to_detect_loco_deck()
527
+
528
+ def _request_param_to_detect_loco_deck(self):
529
+ """Send a parameter request to detect if the Loco deck is installed"""
530
+ group = 'deck'
531
+
532
+ def register(group, param):
533
+ if self._is_in_param_toc(group, param):
534
+ logger.debug("Requesting loco deck parameter")
535
+ self._helper.cf.param.add_update_callback(group=group,
536
+ name=param,
537
+ cb=self._cb_param_to_detect_loco_deck_signal.emit)
538
+
539
+ register(group, 'bcLoco')
540
+ register(group, 'bcDWM1000') # For backwards compatibility
541
+
542
+ def _cb_param_to_detect_loco_deck(self, name, value):
543
+ """Callback from the parameter sub system when the Loco deck detection
544
+ parameter has been updated"""
545
+ if value == '1':
546
+ logger.debug("Loco deck installed, enabling LPS tab")
547
+ self._loco_deck_detected()
548
+ else:
549
+ logger.debug("No Loco deck installed")
550
+
551
+ def _loco_deck_detected(self):
552
+ """Called when the loco deck has been detected. Enables the tab,
553
+ starts logging and polling of the memory sub system as well as starts
554
+ timers for updating graphics"""
555
+ if not self.is_loco_deck_active:
556
+ self.is_loco_deck_active = True
534
557
  try:
535
558
  self._register_logblock(
536
559
  "LoPoTab0",
@@ -552,35 +575,58 @@ class LocoPositioningTab(Tab, locopositioning_tab_class):
552
575
  ("ranging", "distance7", "float"),
553
576
  ],
554
577
  self._anchor_range_signal.emit,
555
- self._log_error_signal.emit),
578
+ self._log_error_signal.emit)
556
579
 
557
580
  self._register_logblock(
558
- "LoPoTab2",
581
+ "LoPoSys",
559
582
  [
560
- ("kalman", "stateX", "float"),
561
- ("kalman", "stateY", "float"),
562
- ("kalman", "stateZ", "float"),
583
+ ("loco", "mode", "uint8_t")
563
584
  ],
564
- self._position_signal.emit,
565
- self._log_error_signal.emit),
585
+ self._loco_sys_signal.emit,
586
+ self._log_error_signal.emit,
587
+ update_period=self.UPDATE_PERIOD_LOCO_MODE)
566
588
  except KeyError as e:
567
589
  logger.warning(str(e))
568
590
  except AttributeError as e:
569
591
  logger.warning(str(e))
570
592
 
571
593
  self._start_polling_anchor_pos(self._helper.cf)
594
+ self._enable_mode_buttons(True)
595
+ self._configure_anchor_positions_button.setEnabled(True)
596
+
597
+ self._helper.cf.param.add_update_callback(
598
+ group=self.PARAM_MDOE_GR,
599
+ name=self.PARAM_MODE_NM,
600
+ cb=self._loco_mode_updated)
601
+
602
+ if self.PARAM_MDOE_GR in self._helper.cf.param.values:
603
+ if self.PARAM_MODE_NM in \
604
+ self._helper.cf.param.values[self.PARAM_MDOE_GR]:
605
+ self._loco_mode_updated(
606
+ self.PARAM_MODE,
607
+ self._helper.cf.param.values[self.PARAM_MDOE_GR][
608
+ self.PARAM_MODE_NM])
572
609
 
573
610
  def _disconnected(self, link_uri):
574
611
  """Callback for when the Crazyflie has been disconnected"""
575
612
  logger.debug("Crazyflie disconnected from {}".format(link_uri))
576
613
  self._stop_polling_anchor_pos()
577
-
578
- def _register_logblock(self, logblock_name, variables, data_cb, error_cb):
614
+ self._clear_state()
615
+ self._update_graphics()
616
+ self.is_loco_deck_active = False
617
+ self._update_lps_state(self.LOCO_MODE_UNKNOWN)
618
+ self._enable_mode_buttons(False)
619
+ self._loco_mode_updated('', self.LOCO_MODE_UNKNOWN)
620
+ self._configure_anchor_positions_button.setEnabled(False)
621
+ self._anchor_position_dialog.close()
622
+
623
+ def _register_logblock(self, logblock_name, variables, data_cb, error_cb,
624
+ update_period=UPDATE_PERIOD_LOG):
579
625
  """Register log data to listen for. One logblock can contain a limited
580
626
  number of parameters (6 for floats)."""
581
- lg = LogConfig(logblock_name, self.UPDATE_PERIOD_LOG)
627
+ lg = LogConfig(logblock_name, update_period)
582
628
  for variable in variables:
583
- if self._is_in_toc(variable):
629
+ if self._is_in_log_toc(variable):
584
630
  lg.add_variable('{}.{}'.format(variable[0], variable[1]),
585
631
  variable[2])
586
632
 
@@ -590,25 +636,79 @@ class LocoPositioningTab(Tab, locopositioning_tab_class):
590
636
  lg.start()
591
637
  return lg
592
638
 
593
- def _is_in_toc(self, variable):
639
+ def _is_in_log_toc(self, variable):
594
640
  toc = self._helper.cf.log.toc
595
641
  group = variable[0]
596
642
  param = variable[1]
597
643
  return group in toc.toc and param in toc.toc[group]
598
644
 
645
+ def _is_in_param_toc(self, group, param):
646
+ toc = self._helper.cf.param.toc
647
+ return bool(group in toc.toc and param in toc.toc[group])
648
+
599
649
  def _anchor_range_received(self, timestamp, data, logconf):
600
650
  """Callback from the logging system when a range is updated."""
601
651
  for name, value in data.items():
602
652
  valid, anchor_number = self._parse_range_param_name(name)
603
- if valid:
604
- self._get_anchor(anchor_number).distance = float(value)
653
+ # Only set distance on anchors that we have seen through other
654
+ # messages to avoid creating anchor 0-7 even if they do not exist
655
+ # in a TDoA3 set up for instance
656
+ if self._anchor_exists(anchor_number):
657
+ if valid:
658
+ anchor = self._get_create_anchor(anchor_number)
659
+ anchor.distance = float(value)
660
+
661
+ def _loco_sys_received(self, timestamp, data, logconf):
662
+ """Callback from the logging system when the loco pos sys config
663
+ is updated."""
664
+ if self.PARAM_MODE in data:
665
+ lps_state = data[self.PARAM_MODE]
666
+ if lps_state == self.LOCO_MODE_TDOA2:
667
+ if self._id_anchor_button.isEnabled():
668
+ if self._id_anchor_button.isChecked():
669
+ self._estimated_postion_button.setChecked(True)
670
+ self._id_anchor_button.setEnabled(False)
671
+ else:
672
+ if not self._id_anchor_button.isEnabled():
673
+ self._id_anchor_button.setEnabled(True)
674
+ self._update_lps_state(lps_state)
605
675
 
606
- def _position_received(self, timestamp, data, logconf):
607
- """Callback from the logging system when the position is updated."""
608
- for name, value in data.items():
609
- valid, axis = self._parse_position_param_name(name)
610
- if valid:
611
- self._position[axis] = float(value)
676
+ def _update_ranging_status_indicators(self):
677
+ container = self._anchor_stats_container
678
+
679
+ ids = sorted(self._anchors.keys())
680
+
681
+ # Update existing labels or add new if needed
682
+ count = 0
683
+ for id in ids:
684
+ col = count % 8
685
+ row = int(count / 8)
686
+
687
+ if count < container.count():
688
+ label = container.itemAtPosition(row, col).widget()
689
+ else:
690
+ label = QLabel()
691
+ label.setMinimumSize(30, 0)
692
+ label.setProperty('frameShape', 'QFrame::Box')
693
+ label.setAlignment(Qt.AlignmentFlag.AlignCenter)
694
+ container.addWidget(label, row, col)
695
+
696
+ label.setText(str(id))
697
+
698
+ if self._anchors[id].is_active():
699
+ label.setStyleSheet(STYLE_GREEN_BACKGROUND)
700
+ else:
701
+ label.setStyleSheet(STYLE_RED_BACKGROUND)
702
+
703
+ count += 1
704
+
705
+ # Remove labels if there are too many
706
+ for i in range(count, container.count()):
707
+ col = i % 8
708
+ row = int(i / 8)
709
+
710
+ label = container.itemAtPosition(row, col).widget()
711
+ label.deleteLater()
612
712
 
613
713
  def _logging_error(self, log_conf, msg):
614
714
  """Callback from the log layer when an error occurs"""
@@ -619,24 +719,45 @@ class LocoPositioningTab(Tab, locopositioning_tab_class):
619
719
  def _start_polling_anchor_pos(self, crazyflie):
620
720
  """Set up a timer to poll anchor positions from the memory sub
621
721
  system"""
622
- self._anchor_pos_timer.start()
722
+ if not self._anchor_state_machine:
723
+ self._anchor_state_machine = AnchorStateMachine(
724
+ crazyflie.mem,
725
+ self._anchor_active_id_list_updated_signal.emit,
726
+ None,
727
+ self._anchor_data_updated_signal.emit
728
+ )
729
+ self._anchor_state_timer.start()
623
730
 
624
731
  def _stop_polling_anchor_pos(self):
625
- self._anchor_pos_timer.stop()
732
+ self._anchor_state_timer.stop()
733
+ self._anchor_state_machine = None
734
+
735
+ def _poll_anchor_state(self):
736
+ if self._anchor_state_machine:
737
+ self._anchor_state_machine.poll()
738
+
739
+ def _active_id_list_updated(self, anchor_list):
740
+ """Callback from the anchor state machine when we get a list of active
741
+ anchors"""
742
+ for id, anchor_data in self._anchors.items():
743
+ anchor_data.set_is_active(False)
744
+
745
+ for id in anchor_list:
746
+ anchor_data = self._get_create_anchor(id)
747
+ anchor_data.set_is_active(True)
626
748
 
627
- def _poll_anchor_positions(self):
628
- mems = self._helper.cf.mem.get_mems(MemoryElement.TYPE_LOCO)
629
- if len(mems) > 0:
630
- mems[0].update(self._anchor_position_signal.emit)
749
+ self._update_ranging_status_indicators()
631
750
 
632
- def _anchor_positions_updated(self, mem):
633
- """Callback from the memory sub system when the anchor positions
751
+ def _anchor_data_updated(self, position_dict):
752
+ """Callback from the anchor state machine when the anchor positions
634
753
  are updated"""
635
- for anchor_number, anchor_data in enumerate(mem.anchor_data):
754
+ for id, anchor_data in position_dict.items():
755
+ anchor = self._get_create_anchor(id)
636
756
  if anchor_data.is_valid:
637
- anchor = self._get_anchor(anchor_number)
638
757
  anchor.set_position(anchor_data.position)
639
758
 
759
+ self._update_positions_in_config_dialog()
760
+
640
761
  def _parse_range_param_name(self, name):
641
762
  """Parse a parameter name for a ranging distance and return the number
642
763
  of the anchor. The name is on the format 'ranging.distance4' """
@@ -658,15 +779,111 @@ class LocoPositioningTab(Tab, locopositioning_tab_class):
658
779
  valid = True
659
780
  return (valid, axis)
660
781
 
661
- def _get_anchor(self, anchor_number):
782
+ def _get_create_anchor(self, anchor_number):
662
783
  if anchor_number not in self._anchors:
663
784
  self._anchors[anchor_number] = Anchor()
664
785
  return self._anchors[anchor_number]
665
786
 
787
+ def _anchor_exists(self, anchor_number):
788
+ return anchor_number in self._anchors
789
+
666
790
  def _update_graphics(self):
667
- if self.is_visible():
791
+ if self.is_visible() and self.is_loco_deck_active:
668
792
  anchors = copy.deepcopy(self._anchors)
669
- self._plot_yz.update(anchors, self._position, self._display_mode)
670
- self._plot_xy.update(anchors, self._position, self._display_mode)
671
- self._plot_xz.update(anchors, self._position, self._display_mode)
672
- self._enable_anchor_pos_ui()
793
+ self._plot_3d.update_data(
794
+ anchors,
795
+ self._helper.pose_logger.position,
796
+ self._display_mode)
797
+ self._update_position_label(self._helper.pose_logger.position)
798
+
799
+ def _update_position_label(self, position):
800
+ if len(position) == 3:
801
+ coordinate = "({:0.2f}, {:0.2f}, {:0.2f})".format(
802
+ position[0], position[1], position[2])
803
+ else:
804
+ coordinate = '(0.00, 0.00, 0.00)'
805
+
806
+ self._status_position.setText(coordinate)
807
+
808
+ def _update_lps_state(self, state):
809
+ if state != self._lps_state:
810
+ self._update_lps_state_indicator(self._state_twr,
811
+ state == self.LOCO_MODE_TWR)
812
+ self._update_lps_state_indicator(self._state_tdoa2,
813
+ state == self.LOCO_MODE_TDOA2)
814
+ self._update_lps_state_indicator(self._state_tdoa3,
815
+ state == self.LOCO_MODE_TDOA3)
816
+ self._lps_state = state
817
+
818
+ def _update_lps_state_indicator(self, element, active):
819
+ if active:
820
+ element.setStyleSheet(STYLE_GREEN_BACKGROUND)
821
+ else:
822
+ element.setStyleSheet(STYLE_NO_BACKGROUND)
823
+
824
+ def _enable_mode_buttons(self, enabled):
825
+ self._mode_auto.setEnabled(enabled)
826
+ self._mode_twr.setEnabled(enabled)
827
+ self._mode_tdoa2.setEnabled(enabled)
828
+ self._mode_tdoa3.setEnabled(enabled)
829
+
830
+ def _request_mode(self, enabled, mode):
831
+ if enabled:
832
+ self._helper.cf.param.set_value(self.PARAM_MODE, str(mode))
833
+
834
+ if mode == self.LOCO_MODE_TWR:
835
+ self._switch_mode_to_twr_button.setEnabled(False)
836
+ self._switch_mode_to_tdoa2_button.setEnabled(True)
837
+ self._switch_mode_to_tdoa3_button.setEnabled(True)
838
+ elif mode == self.LOCO_MODE_TDOA2:
839
+ self._switch_mode_to_twr_button.setEnabled(True)
840
+ self._switch_mode_to_tdoa2_button.setEnabled(False)
841
+ self._switch_mode_to_tdoa3_button.setEnabled(True)
842
+ elif mode == self.LOCO_MODE_TDOA3:
843
+ self._switch_mode_to_twr_button.setEnabled(True)
844
+ self._switch_mode_to_tdoa2_button.setEnabled(True)
845
+ self._switch_mode_to_tdoa3_button.setEnabled(False)
846
+ else:
847
+ self._switch_mode_to_twr_button.setEnabled(False)
848
+ self._switch_mode_to_tdoa2_button.setEnabled(False)
849
+ self._switch_mode_to_tdoa3_button.setEnabled(False)
850
+
851
+ def _loco_mode_updated(self, name, value):
852
+ mode = int(value)
853
+ if mode == self.LOCO_MODE_AUTO:
854
+ if not self._mode_auto.isChecked():
855
+ self._mode_auto.setChecked(True)
856
+ elif mode == self.LOCO_MODE_TWR:
857
+ if not self._mode_twr.isChecked():
858
+ self._mode_twr.setChecked(True)
859
+ elif mode == self.LOCO_MODE_TDOA2:
860
+ if not self._mode_tdoa2.isChecked():
861
+ self._mode_tdoa2.setChecked(True)
862
+ elif mode == self.LOCO_MODE_TDOA3:
863
+ if not self._mode_tdoa3.isChecked():
864
+ self._mode_tdoa3.setChecked(True)
865
+ else:
866
+ self._mode_auto.setChecked(False)
867
+ self._mode_twr.setChecked(False)
868
+ self._mode_tdoa2.setChecked(False)
869
+ self._mode_tdoa3.setChecked(False)
870
+
871
+ def _show_anchor_postion_dialog(self):
872
+ self._anchor_position_dialog.show()
873
+
874
+ def _update_positions_in_config_dialog(self):
875
+ positions = {}
876
+
877
+ for id, anchor in self._anchors.items():
878
+ if anchor.is_position_valid():
879
+ positions[id] = (anchor.x, anchor.y, anchor.z)
880
+
881
+ self._anchor_position_dialog.anchor_postions_updated(positions)
882
+
883
+ def write_positions_to_anchors(self, anchor_positions):
884
+ lopo = LoPoAnchor(self._helper.cf)
885
+
886
+ for _ in range(3):
887
+ for id, position in anchor_positions.items():
888
+ lopo.set_position(id, position)
889
+ time.sleep(0.2)