cfclient 2017.4__py3-none-any.whl → 2025.12.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cfclient/__init__.py +16 -11
- cfclient/configs/config.json +4 -3
- cfclient/configs/input/Generic_OS_X.json +1 -0
- cfclient/configs/input/Joystick.json +1 -0
- cfclient/configs/input/PS3_Mode_1.json +1 -0
- cfclient/configs/input/PS3_Mode_2.json +1 -0
- cfclient/configs/input/PS3_Mode_3.json +1 -0
- cfclient/configs/input/PS4_Mode_1.json +1 -0
- cfclient/configs/input/PS4_Mode_2.json +1 -0
- cfclient/configs/input/PS4_shoulder_btns_yaw.json +1 -0
- cfclient/configs/input/xbox360_mode1.json +1 -0
- cfclient/configs/log/PID_tuning/Attitude.json +46 -0
- cfclient/configs/log/PID_tuning/Attitude_rate.json +46 -0
- cfclient/configs/log/PID_tuning/Position.json +46 -0
- cfclient/configs/log/PID_tuning/Velocity.json +46 -0
- cfclient/configs/log/PID_tuning_components/Pitch.json +22 -0
- cfclient/configs/log/PID_tuning_components/Pitch_rate.json +22 -0
- cfclient/configs/log/PID_tuning_components/Position_x.json +22 -0
- cfclient/configs/log/PID_tuning_components/Position_y.json +22 -0
- cfclient/configs/log/PID_tuning_components/Position_z.json +22 -0
- cfclient/configs/log/PID_tuning_components/Roll.json +22 -0
- cfclient/configs/log/PID_tuning_components/Roll_rate.json +22 -0
- cfclient/configs/log/PID_tuning_components/Velocity_x.json +22 -0
- cfclient/configs/log/PID_tuning_components/Velocity_y.json +22 -0
- cfclient/configs/log/PID_tuning_components/Velocity_z.json +22 -0
- cfclient/configs/log/PID_tuning_components/Yaw.json +22 -0
- cfclient/configs/log/PID_tuning_components/Yaw_rate.json +22 -0
- cfclient/gui.py +44 -9
- cfclient/headless.py +3 -12
- cfclient/resources/log_param_doc.json +1 -0
- cfclient/ui/connectivity_manager.py +198 -0
- cfclient/ui/dialogs/about.py +53 -36
- cfclient/ui/dialogs/about.ui +23 -3
- cfclient/ui/dialogs/anchor_position_dialog.py +252 -0
- cfclient/ui/dialogs/anchor_position_dialog.ui +138 -0
- cfclient/ui/dialogs/basestation_mode_dialog.py +185 -0
- cfclient/ui/dialogs/basestation_mode_dialog.ui +186 -0
- cfclient/ui/dialogs/bootloader.py +448 -85
- cfclient/ui/dialogs/bootloader.ui +387 -134
- cfclient/ui/dialogs/cf2config.py +4 -4
- cfclient/ui/dialogs/cf2config.ui +3 -4
- cfclient/ui/dialogs/inputconfigdialogue.py +24 -19
- cfclient/ui/dialogs/inputconfigdialogue.ui +53 -30
- cfclient/ui/dialogs/lighthouse_bs_geometry_dialog.py +220 -0
- cfclient/ui/dialogs/lighthouse_bs_geometry_dialog.ui +110 -0
- cfclient/ui/dialogs/lighthouse_system_type_dialog.py +93 -0
- cfclient/ui/dialogs/lighthouse_system_type_dialog.ui +121 -0
- cfclient/ui/dialogs/logconfigdialogue.py +401 -101
- cfclient/ui/dialogs/logconfigdialogue.ui +117 -72
- cfclient/ui/icons/bl.webp +0 -0
- cfclient/ui/icons/bolt.webp +0 -0
- cfclient/ui/icons/cf21.webp +0 -0
- cfclient/ui/icons/checkmark_black.png +0 -0
- cfclient/ui/icons/checkmark_white.png +0 -0
- cfclient/ui/icons/create.png +0 -0
- cfclient/ui/icons/delete.png +0 -0
- cfclient/ui/icons/flapper.webp +0 -0
- cfclient/ui/icons/tag.webp +0 -0
- cfclient/ui/main.py +328 -258
- cfclient/ui/main.ui +184 -80
- cfclient/ui/pluginhelper.py +7 -1
- cfclient/ui/pose_logger.py +116 -0
- cfclient/ui/tab_toolbox.py +208 -0
- cfclient/ui/tabs/ColorLEDTab.py +752 -0
- cfclient/ui/tabs/ConsoleTab.py +48 -13
- cfclient/ui/{toolboxes → tabs}/CrtpSharkToolbox.py +19 -34
- cfclient/ui/tabs/ExampleTab.py +9 -16
- cfclient/ui/tabs/FlightTab.py +437 -325
- cfclient/ui/tabs/GpsTab.py +14 -20
- cfclient/ui/tabs/LEDRingTab.py +277 -0
- cfclient/ui/tabs/LogBlockDebugTab.py +20 -27
- cfclient/ui/tabs/LogBlockTab.py +35 -35
- cfclient/ui/tabs/LogClientTab.py +85 -0
- cfclient/ui/tabs/LogTab.py +50 -27
- cfclient/ui/tabs/ParamTab.py +443 -57
- cfclient/ui/tabs/PlotTab.py +23 -25
- cfclient/ui/tabs/TuningTab.py +292 -0
- cfclient/ui/tabs/__init__.py +12 -2
- cfclient/ui/tabs/colorLEDTab.ui +624 -0
- cfclient/ui/tabs/consoleTab.ui +46 -0
- cfclient/ui/tabs/flightActionContainer.ui +103 -0
- cfclient/ui/tabs/flightTab.ui +724 -237
- cfclient/ui/tabs/{ledTab.ui → ledRingTab.ui} +63 -46
- cfclient/ui/tabs/lighthouse_tab.py +714 -0
- cfclient/ui/tabs/lighthouse_tab.ui +430 -0
- cfclient/ui/tabs/locopositioning_tab.py +606 -389
- cfclient/ui/tabs/locopositioning_tab.ui +370 -253
- cfclient/ui/tabs/logClientTab.ui +52 -0
- cfclient/ui/tabs/logTab.ui +1 -1
- cfclient/ui/tabs/paramTab.ui +204 -3
- cfclient/ui/tabs/tuningTab.ui +773 -0
- cfclient/ui/widgets/ai.py +37 -39
- cfclient/ui/widgets/hexspinbox.py +16 -10
- cfclient/ui/widgets/plotter.ui +39 -47
- cfclient/ui/widgets/plotwidget.py +57 -22
- cfclient/ui/widgets/super_slider.py +112 -0
- cfclient/ui/wizards/__init__.py +0 -0
- cfclient/ui/wizards/bslh_1.png +0 -0
- cfclient/ui/wizards/bslh_2.png +0 -0
- cfclient/ui/wizards/bslh_3.png +0 -0
- cfclient/ui/wizards/bslh_4.png +0 -0
- cfclient/ui/wizards/bslh_5.png +0 -0
- cfclient/ui/wizards/lighthouse_geo_bs_estimation_wizard.py +465 -0
- cfclient/utils/config_manager.py +5 -4
- cfclient/utils/input/__init__.py +77 -19
- cfclient/utils/input/inputinterfaces/wiimote.py +2 -2
- cfclient/utils/input/inputreaderinterface.py +17 -7
- cfclient/utils/input/inputreaders/__init__.py +17 -0
- cfclient/utils/logconfigreader.py +245 -25
- cfclient/utils/logdatawriter.py +3 -1
- cfclient/utils/periodictimer.py +1 -1
- cfclient/utils/ui.py +336 -0
- cfclient/utils/zmq_led_driver.py +5 -0
- cfclient/utils/zmq_param.py +6 -0
- cfclient/version.py +34 -1
- cfclient-2025.12.1.dist-info/METADATA +70 -0
- cfclient-2025.12.1.dist-info/RECORD +152 -0
- {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/WHEEL +1 -1
- {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/entry_points.txt +0 -1
- cfclient-2025.12.1.dist-info/licenses/LICENSE.txt +350 -0
- {cfclient-2017.4.dist-info → cfclient-2025.12.1.dist-info}/top_level.txt +1 -0
- cfconfig/Makefile +51 -0
- cfconfig/configblock.py +111 -0
- cfloader/__init__.py +41 -55
- cfzmq/__init__.py +22 -14
- cfclient/ui/dialogs/cf1config.py +0 -265
- cfclient/ui/dialogs/cf1config.ui +0 -260
- cfclient/ui/tab.py +0 -96
- cfclient/ui/tabs/LEDTab.py +0 -169
- cfclient/ui/toolboxes/ConsoleToolbox.py +0 -69
- cfclient/ui/toolboxes/DebugDriverToolbox.py +0 -107
- cfclient/ui/toolboxes/__init__.py +0 -45
- cfclient/ui/toolboxes/consoleToolbox.ui +0 -62
- cfclient/ui/toolboxes/debugDriverToolbox.ui +0 -86
- cfclient-2017.4.dist-info/DESCRIPTION.rst +0 -3
- cfclient-2017.4.dist-info/METADATA +0 -22
- cfclient-2017.4.dist-info/RECORD +0 -104
- cfclient-2017.4.dist-info/metadata.json +0 -1
- /cfclient/{icon-256.png → ui/icons/icon-256.png} +0 -0
- /cfclient/ui/{toolboxes → tabs}/crtpSharkToolbox.ui +0 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
|
|
8
8
|
# || || /_____/_/\__/\___/_/ \__,_/ /___/\___/
|
|
9
9
|
#
|
|
10
|
-
# Copyright (C) 2011-
|
|
10
|
+
# Copyright (C) 2011-2023 Bitcraze AB
|
|
11
11
|
#
|
|
12
12
|
# Crazyflie Nano Quadcopter Client
|
|
13
13
|
#
|
|
@@ -34,43 +34,36 @@ import logging
|
|
|
34
34
|
from enum import Enum
|
|
35
35
|
from collections import namedtuple
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
from
|
|
39
|
-
from
|
|
40
|
-
from
|
|
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.
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 =
|
|
120
|
-
LABEL_HIGHLIGHT_SIZE =
|
|
118
|
+
LABEL_SIZE = 100
|
|
119
|
+
LABEL_HIGHLIGHT_SIZE = 200
|
|
121
120
|
|
|
122
121
|
ANCHOR_SIZE = 10
|
|
123
122
|
HIGHLIGHT_SIZE = 20
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
self.
|
|
130
|
-
|
|
131
|
-
self.
|
|
132
|
-
self.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
self.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
self.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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 <
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
262
|
-
|
|
263
|
-
self.
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
self.tabWidget = tabWidget
|
|
375
|
+
_anchor_active_id_list_updated_signal = pyqtSignal(object)
|
|
376
|
+
_anchor_data_updated_signal = pyqtSignal(object)
|
|
294
377
|
|
|
295
|
-
|
|
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.
|
|
310
|
-
self.
|
|
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.
|
|
402
|
+
self._id_anchor_button.toggled.connect(
|
|
313
403
|
lambda enabled:
|
|
314
|
-
self.
|
|
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.
|
|
410
|
+
self._estimated_postion_button.toggled.connect(
|
|
318
411
|
lambda enabled:
|
|
319
|
-
self.
|
|
412
|
+
self._do_when_checked(
|
|
413
|
+
enabled,
|
|
414
|
+
self._set_display_mode,
|
|
415
|
+
DisplayMode.estimated_position)
|
|
320
416
|
)
|
|
321
417
|
|
|
322
|
-
self.
|
|
323
|
-
|
|
324
|
-
|
|
418
|
+
self._mode_auto.toggled.connect(
|
|
419
|
+
lambda enabled: self._request_mode(enabled, self.LOCO_MODE_AUTO)
|
|
420
|
+
)
|
|
325
421
|
|
|
326
|
-
self.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
353
|
-
self.
|
|
354
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
484
|
+
self._anchor_position_dialog = AnchorPositionDialog(self, helper)
|
|
485
|
+
self._configure_anchor_positions_button.setEnabled(False)
|
|
377
486
|
|
|
378
|
-
def
|
|
379
|
-
|
|
380
|
-
|
|
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.
|
|
385
|
-
self.
|
|
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
|
|
421
|
-
|
|
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
|
-
|
|
517
|
-
|
|
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
|
-
|
|
520
|
-
|
|
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
|
-
|
|
523
|
-
|
|
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
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
if
|
|
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
|
-
"
|
|
581
|
+
"LoPoSys",
|
|
559
582
|
[
|
|
560
|
-
("
|
|
561
|
-
("kalman", "stateY", "float"),
|
|
562
|
-
("kalman", "stateZ", "float"),
|
|
583
|
+
("loco", "mode", "uint8_t")
|
|
563
584
|
],
|
|
564
|
-
self.
|
|
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
|
-
|
|
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,
|
|
627
|
+
lg = LogConfig(logblock_name, update_period)
|
|
582
628
|
for variable in variables:
|
|
583
|
-
if self.
|
|
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
|
|
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
|
-
|
|
604
|
-
|
|
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
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
633
|
-
"""Callback from the
|
|
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
|
|
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
|
|
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.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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)
|