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
cfclient/utils/input/__init__.py
CHANGED
|
@@ -67,6 +67,7 @@ MAX_THRUST = 65000
|
|
|
67
67
|
INITAL_TAGET_HEIGHT = 0.4
|
|
68
68
|
MAX_TARGET_HEIGHT = 1.0
|
|
69
69
|
MIN_TARGET_HEIGHT = 0.03
|
|
70
|
+
MIN_HOVER_HEIGHT = 0.20
|
|
70
71
|
INPUT_READ_PERIOD = 0.01
|
|
71
72
|
|
|
72
73
|
|
|
@@ -80,6 +81,7 @@ class JoystickReader(object):
|
|
|
80
81
|
ASSISTED_CONTROL_ALTHOLD = 0
|
|
81
82
|
ASSISTED_CONTROL_POSHOLD = 1
|
|
82
83
|
ASSISTED_CONTROL_HEIGHTHOLD = 2
|
|
84
|
+
ASSISTED_CONTROL_HOVER = 3
|
|
83
85
|
|
|
84
86
|
def __init__(self, do_device_discovery=True):
|
|
85
87
|
self._input_device = None
|
|
@@ -95,6 +97,7 @@ class JoystickReader(object):
|
|
|
95
97
|
self.thrust_slew_enabled = False
|
|
96
98
|
self.thrust_slew_limit = 0
|
|
97
99
|
self.has_pressure_sensor = False
|
|
100
|
+
self._hover_max_height = MAX_TARGET_HEIGHT
|
|
98
101
|
|
|
99
102
|
self.max_rp_angle = 0
|
|
100
103
|
self.max_yaw_rate = 0
|
|
@@ -110,10 +113,11 @@ class JoystickReader(object):
|
|
|
110
113
|
|
|
111
114
|
self.trim_roll = Config().get("trim_roll")
|
|
112
115
|
self.trim_pitch = Config().get("trim_pitch")
|
|
116
|
+
self._rp_dead_band = 0.1
|
|
113
117
|
|
|
114
118
|
self._input_map = None
|
|
115
119
|
|
|
116
|
-
if Config().get("flightmode")
|
|
120
|
+
if Config().get("flightmode") == "Normal":
|
|
117
121
|
self.max_yaw_rate = Config().get("normal_max_yaw")
|
|
118
122
|
self.max_rp_angle = Config().get("normal_max_rp")
|
|
119
123
|
# Values are stored at %, so use the functions to set the values
|
|
@@ -165,8 +169,10 @@ class JoystickReader(object):
|
|
|
165
169
|
self.input_updated = Caller()
|
|
166
170
|
self.assisted_input_updated = Caller()
|
|
167
171
|
self.heighthold_input_updated = Caller()
|
|
172
|
+
self.hover_input_updated = Caller()
|
|
168
173
|
self.rp_trim_updated = Caller()
|
|
169
174
|
self.emergency_stop_updated = Caller()
|
|
175
|
+
self.arm_updated = Caller()
|
|
170
176
|
self.device_discovery = Caller()
|
|
171
177
|
self.device_error = Caller()
|
|
172
178
|
self.assisted_control_updated = Caller()
|
|
@@ -183,6 +189,9 @@ class JoystickReader(object):
|
|
|
183
189
|
return d
|
|
184
190
|
return None
|
|
185
191
|
|
|
192
|
+
def set_hover_max_height(self, height):
|
|
193
|
+
self._hover_max_height = height
|
|
194
|
+
|
|
186
195
|
def set_alt_hold_available(self, available):
|
|
187
196
|
"""Set if altitude hold is available or not (depending on HW)"""
|
|
188
197
|
self.has_pressure_sensor = available
|
|
@@ -295,13 +304,17 @@ class JoystickReader(object):
|
|
|
295
304
|
|
|
296
305
|
def set_input_map(self, device_name, input_map_name):
|
|
297
306
|
"""Load and set an input device map with the given name"""
|
|
307
|
+
dev = self._get_device_from_name(device_name)
|
|
298
308
|
settings = ConfigManager().get_settings(input_map_name)
|
|
309
|
+
|
|
299
310
|
if settings:
|
|
300
311
|
self.springy_throttle = settings["springythrottle"]
|
|
312
|
+
self._rp_dead_band = settings["rp_dead_band"]
|
|
301
313
|
self._input_map = ConfigManager().get_config(input_map_name)
|
|
302
|
-
|
|
303
|
-
|
|
314
|
+
dev.input_map = self._input_map
|
|
315
|
+
dev.input_map_name = input_map_name
|
|
304
316
|
Config().get("device_config_mapping")[device_name] = input_map_name
|
|
317
|
+
dev.set_dead_band(self._rp_dead_band)
|
|
305
318
|
|
|
306
319
|
def start_input(self, device_name, role="Device", config_name=None):
|
|
307
320
|
"""
|
|
@@ -356,17 +369,30 @@ class JoystickReader(object):
|
|
|
356
369
|
if data:
|
|
357
370
|
if data.toggled.assistedControl:
|
|
358
371
|
if self._assisted_control == \
|
|
359
|
-
JoystickReader.ASSISTED_CONTROL_POSHOLD
|
|
360
|
-
|
|
372
|
+
JoystickReader.ASSISTED_CONTROL_POSHOLD or \
|
|
373
|
+
self._assisted_control == \
|
|
374
|
+
JoystickReader.ASSISTED_CONTROL_HOVER:
|
|
375
|
+
if data.assistedControl and self._assisted_control != \
|
|
376
|
+
JoystickReader.ASSISTED_CONTROL_HOVER:
|
|
361
377
|
for d in self._selected_mux.devices():
|
|
362
378
|
d.limit_thrust = False
|
|
363
379
|
d.limit_rp = False
|
|
380
|
+
elif data.assistedControl:
|
|
381
|
+
for d in self._selected_mux.devices():
|
|
382
|
+
d.limit_thrust = True
|
|
383
|
+
d.limit_rp = False
|
|
364
384
|
else:
|
|
365
385
|
for d in self._selected_mux.devices():
|
|
366
386
|
d.limit_thrust = True
|
|
367
387
|
d.limit_rp = True
|
|
368
388
|
if self._assisted_control == \
|
|
369
|
-
JoystickReader.
|
|
389
|
+
JoystickReader.ASSISTED_CONTROL_ALTHOLD:
|
|
390
|
+
self.assisted_control_updated.call(
|
|
391
|
+
data.assistedControl)
|
|
392
|
+
if ((self._assisted_control ==
|
|
393
|
+
JoystickReader.ASSISTED_CONTROL_HEIGHTHOLD) or
|
|
394
|
+
(self._assisted_control ==
|
|
395
|
+
JoystickReader.ASSISTED_CONTROL_HOVER)):
|
|
370
396
|
try:
|
|
371
397
|
self.assisted_control_updated.call(
|
|
372
398
|
data.assistedControl)
|
|
@@ -379,6 +405,9 @@ class JoystickReader(object):
|
|
|
379
405
|
self.heighthold_input_updated.\
|
|
380
406
|
call(0, 0,
|
|
381
407
|
0, INITAL_TAGET_HEIGHT)
|
|
408
|
+
self.hover_input_updated.\
|
|
409
|
+
call(0, 0,
|
|
410
|
+
0, INITAL_TAGET_HEIGHT)
|
|
382
411
|
except Exception as e:
|
|
383
412
|
logger.warning(
|
|
384
413
|
"Exception while doing callback from "
|
|
@@ -391,7 +420,12 @@ class JoystickReader(object):
|
|
|
391
420
|
except Exception as e:
|
|
392
421
|
logger.warning("Exception while doing callback from"
|
|
393
422
|
"input-device for estop: {}".format(e))
|
|
394
|
-
|
|
423
|
+
if data.toggled.arm and data._prev_btn_values["arm"]:
|
|
424
|
+
try:
|
|
425
|
+
self.arm_updated.call(data.arm)
|
|
426
|
+
except Exception as e:
|
|
427
|
+
logger.warning("Exception while doing callback from"
|
|
428
|
+
"input-device for arm: {}".format(e))
|
|
395
429
|
if data.toggled.alt1:
|
|
396
430
|
try:
|
|
397
431
|
self.alt1_updated.call(data.alt1)
|
|
@@ -406,8 +440,11 @@ class JoystickReader(object):
|
|
|
406
440
|
"input-device for alt2: {}".format(e))
|
|
407
441
|
|
|
408
442
|
# Reset height target when height-hold is not selected
|
|
409
|
-
if not data.assistedControl or
|
|
410
|
-
|
|
443
|
+
if not data.assistedControl or \
|
|
444
|
+
(self._assisted_control !=
|
|
445
|
+
JoystickReader.ASSISTED_CONTROL_HEIGHTHOLD and
|
|
446
|
+
self._assisted_control !=
|
|
447
|
+
JoystickReader.ASSISTED_CONTROL_HOVER):
|
|
411
448
|
self._target_height = INITAL_TAGET_HEIGHT
|
|
412
449
|
|
|
413
450
|
if self._assisted_control == \
|
|
@@ -416,20 +453,41 @@ class JoystickReader(object):
|
|
|
416
453
|
vx = data.roll
|
|
417
454
|
vy = data.pitch
|
|
418
455
|
vz = data.thrust
|
|
419
|
-
yawrate = data.yaw
|
|
456
|
+
yawrate = -data.yaw
|
|
420
457
|
# The odd use of vx and vy is to map forward on the
|
|
421
|
-
# physical joystick to
|
|
458
|
+
# physical joystick to positive X-axis
|
|
422
459
|
self.assisted_input_updated.call(vy, -vx, vz, yawrate)
|
|
460
|
+
elif self._assisted_control == \
|
|
461
|
+
JoystickReader.ASSISTED_CONTROL_HOVER \
|
|
462
|
+
and data.assistedControl:
|
|
463
|
+
vx = data.roll
|
|
464
|
+
vy = data.pitch
|
|
465
|
+
|
|
466
|
+
# Scale thrust to a value between -1.0 to 1.0
|
|
467
|
+
vz = (data.thrust - 32767) / 32767.0
|
|
468
|
+
# Integrate velocity setpoint
|
|
469
|
+
self._target_height += vz * INPUT_READ_PERIOD
|
|
470
|
+
# Cap target height
|
|
471
|
+
if self._target_height > self._hover_max_height:
|
|
472
|
+
self._target_height = self._hover_max_height
|
|
473
|
+
if self._target_height < MIN_HOVER_HEIGHT:
|
|
474
|
+
self._target_height = MIN_HOVER_HEIGHT
|
|
475
|
+
|
|
476
|
+
yawrate = -data.yaw
|
|
477
|
+
# The odd use of vx and vy is to map forward on the
|
|
478
|
+
# physical joystick to positive X-axis
|
|
479
|
+
self.hover_input_updated.call(vy, -vx, yawrate,
|
|
480
|
+
self._target_height)
|
|
423
481
|
else:
|
|
424
482
|
# Update the user roll/pitch trim from device
|
|
425
483
|
if data.toggled.pitchNeg and data.pitchNeg:
|
|
426
|
-
self.trim_pitch -=
|
|
484
|
+
self.trim_pitch -= .2
|
|
427
485
|
if data.toggled.pitchPos and data.pitchPos:
|
|
428
|
-
self.trim_pitch +=
|
|
486
|
+
self.trim_pitch += .2
|
|
429
487
|
if data.toggled.rollNeg and data.rollNeg:
|
|
430
|
-
self.trim_roll -=
|
|
488
|
+
self.trim_roll -= .2
|
|
431
489
|
if data.toggled.rollPos and data.rollPos:
|
|
432
|
-
self.trim_roll +=
|
|
490
|
+
self.trim_roll += .2
|
|
433
491
|
|
|
434
492
|
if data.toggled.pitchNeg or data.toggled.pitchPos or \
|
|
435
493
|
data.toggled.rollNeg or data.toggled.rollPos:
|
|
@@ -441,14 +499,14 @@ class JoystickReader(object):
|
|
|
441
499
|
and data.assistedControl:
|
|
442
500
|
roll = data.roll + self.trim_roll
|
|
443
501
|
pitch = data.pitch + self.trim_pitch
|
|
444
|
-
yawrate = data.yaw
|
|
502
|
+
yawrate = -data.yaw
|
|
445
503
|
# Scale thrust to a value between -1.0 to 1.0
|
|
446
504
|
vz = (data.thrust - 32767) / 32767.0
|
|
447
|
-
# Integrate
|
|
505
|
+
# Integrate velocity setpoint
|
|
448
506
|
self._target_height += vz * INPUT_READ_PERIOD
|
|
449
507
|
# Cap target height
|
|
450
|
-
if self._target_height >
|
|
451
|
-
self._target_height =
|
|
508
|
+
if self._target_height > self._hover_max_height:
|
|
509
|
+
self._target_height = self._hover_max_height
|
|
452
510
|
if self._target_height < MIN_TARGET_HEIGHT:
|
|
453
511
|
self._target_height = MIN_TARGET_HEIGHT
|
|
454
512
|
self.heighthold_input_updated.call(roll, -pitch,
|
|
@@ -24,7 +24,7 @@ class _Reader(object):
|
|
|
24
24
|
|
|
25
25
|
def devices(self):
|
|
26
26
|
"""List all the available connections"""
|
|
27
|
-
raise
|
|
27
|
+
raise NotImplementedError
|
|
28
28
|
|
|
29
29
|
def open(self, device_id):
|
|
30
30
|
"""
|
|
@@ -38,7 +38,7 @@ class _Reader(object):
|
|
|
38
38
|
|
|
39
39
|
def read(self, device_id):
|
|
40
40
|
"""Read input from the selected device."""
|
|
41
|
-
raise
|
|
41
|
+
raise NotImplementedError
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
TWO = 1
|
|
@@ -50,9 +50,9 @@ class InputData:
|
|
|
50
50
|
def __init__(self):
|
|
51
51
|
# self._toggled = {}
|
|
52
52
|
self._axes = ("roll", "pitch", "yaw", "thrust")
|
|
53
|
-
self._buttons = ("
|
|
54
|
-
"
|
|
55
|
-
"muxswitch")
|
|
53
|
+
self._buttons = ("pitchNeg", "pitchPos", "rollNeg", "rollPos",
|
|
54
|
+
"assistedControl", "estop", "arm",
|
|
55
|
+
"exitapp", "alt1", "alt2", "muxswitch")
|
|
56
56
|
for axis in self._axes:
|
|
57
57
|
self.__dict__[axis] = 0.0
|
|
58
58
|
self.toggled = _ToggleState()
|
|
@@ -165,15 +165,25 @@ class InputReaderInterface(object):
|
|
|
165
165
|
self.input.max_yaw_rate)
|
|
166
166
|
|
|
167
167
|
def _limit_thrust(self, thrust, assisted_control, emergency_stop):
|
|
168
|
-
#
|
|
168
|
+
# Thrust limiting (slew, minimum and emergency stop)
|
|
169
|
+
|
|
170
|
+
current_time = time()
|
|
169
171
|
if self.input.springy_throttle:
|
|
170
172
|
if assisted_control and \
|
|
171
173
|
(self.input.get_assisted_control() ==
|
|
172
174
|
self.input.ASSISTED_CONTROL_ALTHOLD or
|
|
173
175
|
self.input.get_assisted_control() ==
|
|
174
|
-
self.input.ASSISTED_CONTROL_HEIGHTHOLD
|
|
176
|
+
self.input.ASSISTED_CONTROL_HEIGHTHOLD or
|
|
177
|
+
self.input.get_assisted_control() ==
|
|
178
|
+
self.input.ASSISTED_CONTROL_HOVER):
|
|
175
179
|
thrust = int(round(InputReaderInterface.deadband(thrust, 0.2) *
|
|
176
180
|
32767 + 32767)) # Convert to uint16
|
|
181
|
+
|
|
182
|
+
# do not drop thrust to 0 after switching hover mode off
|
|
183
|
+
# set previous values for slew limit logic
|
|
184
|
+
self._prev_thrust = self.input.thrust_slew_limit
|
|
185
|
+
self._last_time = current_time
|
|
186
|
+
|
|
177
187
|
else:
|
|
178
188
|
# Scale the thrust to percent (it's between 0 and 1)
|
|
179
189
|
thrust *= 100
|
|
@@ -197,7 +207,7 @@ class InputReaderInterface(object):
|
|
|
197
207
|
else:
|
|
198
208
|
# If we are "inside" the limit, then lower
|
|
199
209
|
# according to the rate we have set each iteration
|
|
200
|
-
lowering = ((
|
|
210
|
+
lowering = ((current_time - self._last_time) *
|
|
201
211
|
self.input.thrust_slew_rate)
|
|
202
212
|
limited_thrust = self._prev_thrust - lowering
|
|
203
213
|
elif emergency_stop or thrust < self.thrust_stop_limit:
|
|
@@ -216,7 +226,7 @@ class InputReaderInterface(object):
|
|
|
216
226
|
self._prev_thrust = 0
|
|
217
227
|
limited_thrust = 0
|
|
218
228
|
|
|
219
|
-
self._last_time =
|
|
229
|
+
self._last_time = current_time
|
|
220
230
|
|
|
221
231
|
thrust = limited_thrust
|
|
222
232
|
else:
|
|
@@ -92,6 +92,7 @@ class InputDevice(InputReaderInterface):
|
|
|
92
92
|
self.limit_rp = True
|
|
93
93
|
self.limit_thrust = True
|
|
94
94
|
self.limit_yaw = True
|
|
95
|
+
self.db = 0.
|
|
95
96
|
|
|
96
97
|
def open(self):
|
|
97
98
|
# TODO: Reset data?
|
|
@@ -100,6 +101,9 @@ class InputDevice(InputReaderInterface):
|
|
|
100
101
|
def close(self):
|
|
101
102
|
self._reader.close(self.id)
|
|
102
103
|
|
|
104
|
+
def set_dead_band(self, db):
|
|
105
|
+
self.db = db
|
|
106
|
+
|
|
103
107
|
def read(self, include_raw=False):
|
|
104
108
|
[axis, buttons] = self._reader.read(self.id)
|
|
105
109
|
|
|
@@ -135,6 +139,9 @@ class InputDevice(InputReaderInterface):
|
|
|
135
139
|
pass
|
|
136
140
|
i += 1
|
|
137
141
|
|
|
142
|
+
self.data.roll = InputDevice.deadband(self.data.roll, self.db)
|
|
143
|
+
self.data.pitch = InputDevice.deadband(self.data.pitch, self.db)
|
|
144
|
+
|
|
138
145
|
if self.limit_rp:
|
|
139
146
|
[self.data.roll, self.data.pitch] = self._scale_rp(self.data.roll,
|
|
140
147
|
self.data.pitch)
|
|
@@ -149,3 +156,13 @@ class InputDevice(InputReaderInterface):
|
|
|
149
156
|
return [axis, buttons, self.data]
|
|
150
157
|
else:
|
|
151
158
|
return self.data
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def deadband(value, threshold):
|
|
162
|
+
if abs(value) < threshold:
|
|
163
|
+
value = 0
|
|
164
|
+
elif value > 0:
|
|
165
|
+
value -= threshold
|
|
166
|
+
elif value < 0:
|
|
167
|
+
value += threshold
|
|
168
|
+
return value / (1 - threshold)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
|
|
8
8
|
# || || /_____/_/\__/\___/_/ \__,_/ /___/\___/
|
|
9
9
|
#
|
|
10
|
-
# Copyright (C) 2011-
|
|
10
|
+
# Copyright (C) 2011-2024 Bitcraze AB
|
|
11
11
|
#
|
|
12
12
|
# Crazyflie Nano Quadcopter Client
|
|
13
13
|
#
|
|
@@ -27,59 +27,267 @@
|
|
|
27
27
|
# MA 02110-1301, USA.
|
|
28
28
|
|
|
29
29
|
"""
|
|
30
|
-
The
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
This module can use different drivers for reading the input device data.
|
|
34
|
-
Currently it can just use the PySdl2 driver but in the future there will be a
|
|
35
|
-
Linux and Windows driver that can bypass PySdl2.
|
|
30
|
+
The logconfigreader module is responsible for reading and parsing log
|
|
31
|
+
configuration data. This data is used to control what information is logged
|
|
32
|
+
in the client.
|
|
36
33
|
"""
|
|
37
34
|
|
|
38
|
-
import glob
|
|
39
35
|
import json
|
|
40
36
|
import logging
|
|
41
37
|
import os
|
|
38
|
+
import re
|
|
42
39
|
import shutil
|
|
43
40
|
|
|
44
41
|
import cfclient
|
|
45
42
|
from cflib.crazyflie.log import LogVariable, LogConfig
|
|
46
43
|
|
|
44
|
+
from PyQt6 import QtGui
|
|
45
|
+
|
|
47
46
|
__author__ = 'Bitcraze AB'
|
|
48
47
|
__all__ = ['LogVariable', 'LogConfigReader']
|
|
49
48
|
|
|
50
49
|
logger = logging.getLogger(__name__)
|
|
51
50
|
|
|
51
|
+
DEFAULT_CONF_NAME = 'log_config'
|
|
52
|
+
DEFAULT_CATEGORY_NAME = 'category'
|
|
53
|
+
|
|
54
|
+
FILE_REGEX_YAML = "Config *.yaml;;All *.*"
|
|
55
|
+
|
|
52
56
|
|
|
53
57
|
class LogConfigReader():
|
|
54
58
|
"""Reads logging configurations from file"""
|
|
55
59
|
|
|
56
60
|
def __init__(self, crazyflie):
|
|
61
|
+
|
|
62
|
+
self._log_configs = {}
|
|
57
63
|
self.dsList = []
|
|
58
64
|
# Check if user config exists, otherwise copy files
|
|
59
65
|
if (not os.path.exists(cfclient.config_path + "/log")):
|
|
60
66
|
logger.info("No user config found, copying dist files")
|
|
61
|
-
|
|
62
|
-
for f in glob.glob(
|
|
63
|
-
cfclient.module_path + "/configs/log/[A-Za-z]*.json"):
|
|
64
|
-
shutil.copy2(f, cfclient.config_path + "/log")
|
|
67
|
+
shutil.copytree(cfclient.module_path + "/configs/log", cfclient.config_path + "/log")
|
|
65
68
|
self._cf = crazyflie
|
|
66
69
|
self._cf.connected.add_callback(self._connected)
|
|
67
70
|
|
|
71
|
+
def get_icons(self):
|
|
72
|
+
client_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
|
73
|
+
os.pardir))
|
|
74
|
+
icon_path = os.path.join(client_path, 'ui', 'icons')
|
|
75
|
+
save_icon = QtGui.QIcon(os.path.join(icon_path, 'create.png'))
|
|
76
|
+
delete_icon = QtGui.QIcon(os.path.join(icon_path, 'delete.png'))
|
|
77
|
+
return save_icon, delete_icon
|
|
78
|
+
|
|
79
|
+
def create_empty_log_conf(self, category):
|
|
80
|
+
""" Creates an empty log-configuration with a default name """
|
|
81
|
+
log_path = self._get_log_path(category)
|
|
82
|
+
conf_name = self._get_default_conf_name(log_path)
|
|
83
|
+
file_path = os.path.join(log_path, conf_name) + '.json'
|
|
84
|
+
|
|
85
|
+
if not os.path.exists(file_path):
|
|
86
|
+
with open(file_path, 'w') as f:
|
|
87
|
+
f.write(json.dumps(
|
|
88
|
+
{
|
|
89
|
+
'logconfig': {
|
|
90
|
+
'logblock': {
|
|
91
|
+
'variables': [],
|
|
92
|
+
'name': conf_name,
|
|
93
|
+
'period': 100
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}, indent=2))
|
|
97
|
+
|
|
98
|
+
self._log_configs[category].append(LogConfig(conf_name, 100))
|
|
99
|
+
return conf_name
|
|
100
|
+
|
|
101
|
+
def create_category(self):
|
|
102
|
+
""" Creates a new category (dir in filesystem), with a unique name """
|
|
103
|
+
log_path = os.path.join(cfclient.config_path, 'log')
|
|
104
|
+
category = self._get_default_category(log_path)
|
|
105
|
+
dir_path = os.path.join(log_path, category)
|
|
106
|
+
|
|
107
|
+
# This should never be false, but just to be safe.
|
|
108
|
+
if not os.path.exists(dir_path):
|
|
109
|
+
os.mkdir(dir_path)
|
|
110
|
+
self._log_configs[category] = []
|
|
111
|
+
|
|
112
|
+
return category
|
|
113
|
+
|
|
114
|
+
def delete_category(self, category):
|
|
115
|
+
""" Removes the directory on file-system and recursively removes
|
|
116
|
+
all the logging configurations.
|
|
117
|
+
"""
|
|
118
|
+
log_path = self._get_log_path(category)
|
|
119
|
+
if os.path.exists(log_path):
|
|
120
|
+
shutil.rmtree(log_path)
|
|
121
|
+
self._log_configs.pop(category)
|
|
122
|
+
|
|
123
|
+
def delete_config(self, conf_name, category):
|
|
124
|
+
""" Deletes a configuration from file system. """
|
|
125
|
+
log_path = self._get_log_path(category)
|
|
126
|
+
conf_path = os.path.join(log_path, conf_name) + '.json'
|
|
127
|
+
|
|
128
|
+
if not os.path.exists(conf_path):
|
|
129
|
+
# Check if we can find the file with lowercase first letter.
|
|
130
|
+
conf_path = os.path.join(log_path,
|
|
131
|
+
conf_name[0].lower() + conf_name[1:]
|
|
132
|
+
+ '.json')
|
|
133
|
+
if not os.path.exists(conf_path):
|
|
134
|
+
# Cant' find the config-file
|
|
135
|
+
logger.warning('Failed to find log-config %s' % conf_path)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
os.remove(conf_path)
|
|
139
|
+
for conf in self._log_configs[category]:
|
|
140
|
+
if conf.name == conf_name:
|
|
141
|
+
self._log_configs[category].remove(conf)
|
|
142
|
+
|
|
143
|
+
def change_name_config(self, old_name, new_name, category):
|
|
144
|
+
""" Changes name to the configuration and updates the
|
|
145
|
+
file in the file system.
|
|
146
|
+
"""
|
|
147
|
+
configs = self._log_configs[category]
|
|
148
|
+
|
|
149
|
+
for conf in configs:
|
|
150
|
+
if conf.name == old_name:
|
|
151
|
+
conf.name = new_name
|
|
152
|
+
|
|
153
|
+
log_path = self._get_log_path(category)
|
|
154
|
+
old_path = os.path.join(log_path, old_name) + '.json'
|
|
155
|
+
new_path = os.path.join(log_path, new_name) + '.json'
|
|
156
|
+
|
|
157
|
+
# File should exist but just to be extra safe
|
|
158
|
+
if os.path.exists(old_path):
|
|
159
|
+
with open(old_path, 'r+') as f:
|
|
160
|
+
data = json.load(f)
|
|
161
|
+
data['logconfig']['logblock']['name'] = new_name
|
|
162
|
+
f.seek(0)
|
|
163
|
+
f.truncate()
|
|
164
|
+
f.write(json.dumps(data, indent=2))
|
|
165
|
+
|
|
166
|
+
os.rename(old_path, new_path)
|
|
167
|
+
|
|
168
|
+
def change_name_category(self, old_name, new_name):
|
|
169
|
+
""" Renames the directory on file system and the config dict """
|
|
170
|
+
if old_name in self._log_configs:
|
|
171
|
+
self._log_configs[new_name] = self._log_configs.pop(old_name)
|
|
172
|
+
os.rename(self._get_log_path(old_name),
|
|
173
|
+
self._get_log_path(new_name))
|
|
174
|
+
|
|
175
|
+
def _get_log_path(self, category):
|
|
176
|
+
""" Helper method """
|
|
177
|
+
category_dir = '' if category == 'Default' else '/' + category
|
|
178
|
+
return os.path.join(cfclient.config_path,
|
|
179
|
+
'log' + category_dir)
|
|
180
|
+
|
|
181
|
+
def _get_default_category(self, log_path):
|
|
182
|
+
""" Creates a name for the category, ending with a unique number. """
|
|
183
|
+
dirs = [dir_ for dir_ in os.listdir(log_path) if os.path.isdir(
|
|
184
|
+
os.path.join(log_path, dir_)
|
|
185
|
+
)]
|
|
186
|
+
config_nbrs = re.findall(r'(?<=%s)\d*' % DEFAULT_CATEGORY_NAME,
|
|
187
|
+
' '.join(dirs))
|
|
188
|
+
config_nbrs = list(filter(len, config_nbrs))
|
|
189
|
+
|
|
190
|
+
if config_nbrs:
|
|
191
|
+
return DEFAULT_CATEGORY_NAME + str(
|
|
192
|
+
max([int(nbr) for nbr in config_nbrs]) + 1)
|
|
193
|
+
else:
|
|
194
|
+
return DEFAULT_CATEGORY_NAME + '1'
|
|
195
|
+
|
|
196
|
+
def _read_config_categories(self):
|
|
197
|
+
"""Read and parse log configurations"""
|
|
198
|
+
|
|
199
|
+
self._log_configs = {'Default': []}
|
|
200
|
+
log_path = os.path.join(cfclient.config_path, 'log')
|
|
201
|
+
|
|
202
|
+
for category in os.listdir(log_path):
|
|
203
|
+
|
|
204
|
+
category_path = os.path.join(log_path, category)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
if (os.path.isdir(category_path)):
|
|
208
|
+
# create a new cathegory
|
|
209
|
+
self._log_configs[category] = []
|
|
210
|
+
for conf in os.listdir(category_path):
|
|
211
|
+
if conf.endswith('.json'):
|
|
212
|
+
conf_path = os.path.join(category_path, conf)
|
|
213
|
+
log_conf = self._get_conf(conf_path)
|
|
214
|
+
|
|
215
|
+
# add the log configuration to the cathegory
|
|
216
|
+
self._log_configs[category].append(log_conf)
|
|
217
|
+
|
|
218
|
+
else:
|
|
219
|
+
# if it's not a directory, the log config is placed
|
|
220
|
+
# in the 'Default' cathegory
|
|
221
|
+
if category_path.endswith('.json'):
|
|
222
|
+
log_conf = self._get_conf(category_path)
|
|
223
|
+
self._log_configs['Default'].append(log_conf)
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.warning("Failed to open log config %s", e)
|
|
227
|
+
|
|
228
|
+
def _get_default_conf_name(self, log_path):
|
|
229
|
+
config_nbrs = re.findall(r'(?<=%s)\d*(?!=\.json)' % DEFAULT_CONF_NAME,
|
|
230
|
+
' '.join(os.listdir(log_path)))
|
|
231
|
+
config_nbrs = list(filter(len, config_nbrs))
|
|
232
|
+
|
|
233
|
+
if config_nbrs:
|
|
234
|
+
return DEFAULT_CONF_NAME + str(
|
|
235
|
+
max([int(nbr) for nbr in config_nbrs]) + 1)
|
|
236
|
+
else:
|
|
237
|
+
return DEFAULT_CONF_NAME + '1'
|
|
238
|
+
|
|
239
|
+
def _get_conf(self, conf_path):
|
|
240
|
+
with open(conf_path) as f:
|
|
241
|
+
data = json.load(f)
|
|
242
|
+
infoNode = data["logconfig"]["logblock"]
|
|
243
|
+
|
|
244
|
+
logConf = LogConfig(infoNode["name"],
|
|
245
|
+
int(infoNode["period"]))
|
|
246
|
+
for v in data["logconfig"]["logblock"]["variables"]:
|
|
247
|
+
if v["type"] == "TOC":
|
|
248
|
+
logConf.add_variable(str(v["name"]), v["fetch_as"])
|
|
249
|
+
else:
|
|
250
|
+
logConf.add_variable("Mem", v["fetch_as"],
|
|
251
|
+
v["stored_as"],
|
|
252
|
+
int(v["address"], 16))
|
|
253
|
+
return logConf
|
|
254
|
+
|
|
255
|
+
def _get_configpaths_recursively(self):
|
|
256
|
+
""" Reads all configuration files from the log path and
|
|
257
|
+
returns a list of tuples with format:
|
|
258
|
+
(category/conf-name, absolute path).
|
|
259
|
+
"""
|
|
260
|
+
logpath = os.path.join(cfclient.config_path, 'log')
|
|
261
|
+
filepaths = []
|
|
262
|
+
|
|
263
|
+
for files in os.listdir(logpath):
|
|
264
|
+
abspath = os.path.join(logpath, files)
|
|
265
|
+
if os.path.isdir(abspath):
|
|
266
|
+
for config in os.listdir(abspath):
|
|
267
|
+
if config.endswith('.json'):
|
|
268
|
+
filepaths.append(('/'.join([files, config]),
|
|
269
|
+
os.path.join(abspath, config)))
|
|
270
|
+
else:
|
|
271
|
+
if files.endswith('.json'):
|
|
272
|
+
filepaths.append((files, os.path.join(abspath)))
|
|
273
|
+
|
|
274
|
+
return filepaths
|
|
275
|
+
|
|
68
276
|
def _read_config_files(self):
|
|
69
277
|
"""Read and parse log configurations"""
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
278
|
+
|
|
279
|
+
configsfound = self._get_configpaths_recursively()
|
|
280
|
+
|
|
73
281
|
new_dsList = []
|
|
74
282
|
for conf in configsfound:
|
|
75
283
|
try:
|
|
76
|
-
logger.info("Parsing [%s]", conf)
|
|
77
|
-
json_data = open(
|
|
284
|
+
logger.info("Parsing [%s]", conf[0])
|
|
285
|
+
json_data = open(conf[1])
|
|
78
286
|
self.data = json.load(json_data)
|
|
79
287
|
infoNode = self.data["logconfig"]["logblock"]
|
|
288
|
+
logConfName = conf[0].replace('.json', '')
|
|
80
289
|
|
|
81
|
-
logConf = LogConfig(infoNode["
|
|
82
|
-
int(infoNode["period"]))
|
|
290
|
+
logConf = LogConfig(logConfName, int(infoNode["period"]))
|
|
83
291
|
for v in self.data["logconfig"]["logblock"]["variables"]:
|
|
84
292
|
if v["type"] == "TOC":
|
|
85
293
|
logConf.add_variable(str(v["name"]), v["fetch_as"])
|
|
@@ -97,6 +305,7 @@ class LogConfigReader():
|
|
|
97
305
|
"""Callback that is called once Crazyflie is connected"""
|
|
98
306
|
|
|
99
307
|
self._read_config_files()
|
|
308
|
+
self._read_config_categories()
|
|
100
309
|
# Just add all the configurations. Via callbacks other parts of the
|
|
101
310
|
# application will pick up these configurations and use them
|
|
102
311
|
for d in self.dsList:
|
|
@@ -111,10 +320,15 @@ class LogConfigReader():
|
|
|
111
320
|
"""Return the log configurations"""
|
|
112
321
|
return self.dsList
|
|
113
322
|
|
|
114
|
-
def
|
|
323
|
+
def _getLogConfigs(self):
|
|
324
|
+
"""Return the log configurations"""
|
|
325
|
+
return self._log_configs
|
|
326
|
+
|
|
327
|
+
def saveLogConfigFile(self, category, logconfig):
|
|
115
328
|
"""Save a log configuration to file"""
|
|
116
|
-
|
|
117
|
-
|
|
329
|
+
log_path = self._get_log_path(category)
|
|
330
|
+
file_path = os.path.join(log_path, logconfig.name) + '.json'
|
|
331
|
+
logger.info("Saving config for [%s]", file_path)
|
|
118
332
|
|
|
119
333
|
# Build tree for JSON
|
|
120
334
|
saveConfig = {}
|
|
@@ -133,6 +347,12 @@ class LogConfigReader():
|
|
|
133
347
|
|
|
134
348
|
saveConfig['logconfig'] = logconf
|
|
135
349
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
350
|
+
for old_conf in self._log_configs[category]:
|
|
351
|
+
if old_conf.name == logconfig.name:
|
|
352
|
+
self._log_configs[category].remove(old_conf)
|
|
353
|
+
self._log_configs[category].append(logconfig)
|
|
354
|
+
|
|
355
|
+
with open(file_path, 'w') as f:
|
|
356
|
+
f.write(json.dumps(saveConfig, indent=2))
|
|
357
|
+
|
|
358
|
+
self._read_config_files()
|