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
@@ -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") is "Normal":
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
- self._get_device_from_name(device_name).input_map = self._input_map
303
- self._get_device_from_name(device_name).input_map_name = input_map_name
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
- if data.assistedControl:
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.ASSISTED_CONTROL_HEIGHTHOLD:
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 self._assisted_control != \
410
- JoystickReader.ASSISTED_CONTROL_HEIGHTHOLD:
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 positiv X-axis
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 -= 1
484
+ self.trim_pitch -= .2
427
485
  if data.toggled.pitchPos and data.pitchPos:
428
- self.trim_pitch += 1
486
+ self.trim_pitch += .2
429
487
  if data.toggled.rollNeg and data.rollNeg:
430
- self.trim_roll -= 1
488
+ self.trim_roll -= .2
431
489
  if data.toggled.rollPos and data.rollPos:
432
- self.trim_roll += 1
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 velosity setpoint
505
+ # Integrate velocity setpoint
448
506
  self._target_height += vz * INPUT_READ_PERIOD
449
507
  # Cap target height
450
- if self._target_height > MAX_TARGET_HEIGHT:
451
- self._target_height = MAX_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 NotImplemented()
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 NotImplemented()
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 = ("alt1", "alt2", "estop", "exit", "pitchNeg",
54
- "pitchPos", "rollNeg", "rollPos", "assistedControl",
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
- # Thust limiting (slew, minimum and emergency stop)
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 = ((time() - self._last_time) *
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 = 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-2013 Bitcraze AB
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 input module that will read joysticks/input devices and send control set-
31
- points to the Crazyflie. It will also accept settings from the UI.
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
- os.makedirs(cfclient.config_path + "/log")
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
- configsfound = [os.path.basename(f) for f in
71
- glob.glob(cfclient.config_path +
72
- "/log/[A-Za-z_-]*.json")]
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(cfclient.config_path + "/log/%s" % conf)
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["name"],
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 saveLogConfigFile(self, logconfig):
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
- filename = cfclient.config_path + "/log/" + logconfig.name + ".json"
117
- logger.info("Saving config for [%s]", filename)
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
- json_data = open(filename, 'w')
137
- json_data.write(json.dumps(saveConfig, indent=2))
138
- json_data.close()
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()