@@ -69,15 +69,22 @@ class DjiCaptain():
69
69
def __init__ (self , node : Node ):
70
70
self ._node = node
71
71
self ._node .declare_parameter ("robot_name" , "M350" )
72
+ self ._node .declare_parameter ("controller_deadzone" , 0.1 )
72
73
73
74
74
- self ._TF_NS = f"{ self ._node .get_parameter ('robot_name' )} /"
75
+ self ._TF_NS : str = f"{ self ._node .get_parameter ('robot_name' )} /"
76
+
77
+ self ._CONTROLLER_DEADZONE : float = 0.1
78
+ v = self ._node .get_parameter ("controller_deadzone" ).value
79
+ if v is not None : self ._CONTROLLER_DEADZONE = v
75
80
76
81
77
82
self ._control_mode = ControlModes .FLUvel
78
83
self ._move_to_setpoint : PoseStamped | None = None
79
84
self ._joy_timer : None | Timer = None
80
- self ._joy_pub = node .create_publisher (Joy , PSDKTopics .FLUvel_JOY .value , qos_profile = 10 )
85
+ self ._FLU_vel_joy_pub = node .create_publisher (Joy , PSDKTopics .FLUvel_JOY .value , qos_profile = 10 )
86
+ self ._ENU_vel_joy_pub = node .create_publisher (Joy , PSDKTopics .ENUvel_JOY .value , qos_profile = 10 )
87
+ self ._ENU_pos_joy_pub = node .create_publisher (Joy , PSDKTopics .ENUpos_JOY .value , qos_profile = 10 )
81
88
self ._setpoint_received_at : float | None = None
82
89
83
90
self .MOVE_TO_SETPOINT_MAX_AGE : float = 0.5 # seconds, how long we keep the move to setpoint before we consider it stale
@@ -253,34 +260,22 @@ def __init__(self, node: Node):
253
260
self ._land_srv = node .create_client (Trigger , PSDKTopics .LAND_SRV .value )
254
261
255
262
256
- while True :
263
+ while rclpy . ok () :
257
264
commands = "Commands:\n "
258
265
commands += " 1: Take control \n "
259
266
commands += " 2: Release control\n "
260
267
commands += " 3: Take off\n "
261
268
commands += " 4: Land\n "
262
- commands += " 5: Print status (also available on $ROBOT_NAME/captain_status topic) \n "
263
- commands += " 8: EXIT \n "
269
+ commands += f"\n 8: Print status (also available on { self ._TF_NS } captain_status topic) \n "
264
270
commands += " 9: Set max joy to (DANGEROUS, DONT USE UNLESS YOUR NAME STARTS WITH O)\n "
271
+ commands += " 0: EXIT \n "
265
272
try :
266
273
self .log (commands )
267
274
n = int (input ("Enter number for command: \n " ))
268
275
if n == 1 : #Take control
269
- if not self ._take_control_srv .wait_for_service (timeout_sec = 5.0 ):
270
- self .log ("Take control service not available..." )
271
- continue
272
- future = self ._take_control_srv .call_async (Trigger .Request ())
273
- future .add_done_callback (
274
- lambda f : self .log (f"Take control service called, success: { f .result ().success } , message: { f .result ().message } " )
275
- )
276
+ self ._take_control ()
276
277
elif n == 2 : #Release control
277
- if not self ._release_control_srv .wait_for_service (timeout_sec = 5.0 ):
278
- self .log ("Release control service not available..." )
279
- continue
280
- future = self ._release_control_srv .call_async (Trigger .Request ())
281
- future .add_done_callback (
282
- lambda f : self .log (f"Release control service called, success: { f .result ().success } , message: { f .result ().message } " )
283
- )
278
+ self ._release_control ()
284
279
elif n == 3 : #Take off
285
280
if self ._got_control is False :
286
281
self .log ("You must take control first!" )
@@ -315,9 +310,13 @@ def __init__(self, node: Node):
315
310
self .log (self .status_str )
316
311
elif n == 9 : # set max joy
317
312
self .JOY_PUB_MAX = float (input ("Enter new max joy value: " ) or "0" )
318
- self .log (f"Set max joy to { self .JOY_PUB_MAX :.2f} (m/s? )" )
313
+ self .log (f"Set max joy to { self .JOY_PUB_MAX :.2f} (m/s)" )
319
314
elif n == 0 :
320
- self .log ("Exiting captain" )
315
+ try :
316
+ self .log ("Exiting captain" )
317
+ self ._speak ("Bye bye." )
318
+ except :
319
+ print ("Exiting captain." )
321
320
break
322
321
323
322
except :
@@ -374,6 +373,41 @@ def log(self, msg: str):
374
373
self ._node .get_logger ().info (msg )
375
374
376
375
376
+ def _take_control (self ):
377
+ def on_result (f ):
378
+ self .log (f"Take control service called, success: { f .result ().success } , message: { f .result ().message } " )
379
+ if f .result ().success :
380
+ self ._speak ("Got control." )
381
+ else :
382
+ self ._speak ("Failed to get control." )
383
+
384
+ self .log ("Taking control." )
385
+ self ._speak ("Taking control." )
386
+ if not self ._take_control_srv .wait_for_service (timeout_sec = 5.0 ):
387
+ self .log ("Take control service not available..." )
388
+ self ._speak ("Take control service not available." )
389
+ return
390
+ future = self ._take_control_srv .call_async (Trigger .Request ())
391
+ future .add_done_callback (on_result )
392
+
393
+
394
+ def _release_control (self ):
395
+ def on_result (f ):
396
+ self .log (f"Release control service called, success: { f .result ().success } , message: { f .result ().message } " )
397
+ if f .result ().success :
398
+ self ._speak ("Gave up control." )
399
+ else :
400
+ self ._speak ("Failed to release control." )
401
+
402
+ self .log ("Releasing control." )
403
+ self ._speak ("Releasing control." )
404
+ if not self ._release_control_srv .wait_for_service (timeout_sec = 5.0 ):
405
+ self .log ("Release control service not available..." )
406
+ self ._speak ("Release control service not available." )
407
+ return
408
+ future = self ._release_control_srv .call_async (Trigger .Request ())
409
+ future .add_done_callback (on_result )
410
+
377
411
378
412
def _geo_alt_cb (self , msg : Float32 ):
379
413
self ._geo_altitude = msg .data
@@ -417,7 +451,13 @@ def _move_to_setpoint_callback(self, msg: PoseStamped):
417
451
self .log (f"Move to setpoint received: { format_pose_stamped (self ._move_to_setpoint )} " )
418
452
419
453
if self ._joy_timer is None :
420
- self ._joy_timer = self ._node .create_timer (self .JOY_PUB_PERIOD , self ._move_towards_setpoint_FLUvel )
454
+ if self ._control_mode == ControlModes .FLUvel :
455
+ self ._joy_timer = self ._node .create_timer (self .JOY_PUB_PERIOD , self ._move_towards_setpoint_FLUvel )
456
+ elif self ._control_mode == ControlModes .ENUvel :
457
+ self ._joy_timer = self ._node .create_timer (self .JOY_PUB_PERIOD , self .move_towards_setpoint_ENUvel )
458
+ elif self ._control_mode == ControlModes .ENUpos :
459
+ self ._joy_timer = self ._node .create_timer (self .JOY_PUB_PERIOD , self .move_towards_setpoint_ENUpos )
460
+
421
461
self ._setpoint_received_at = time .time ()
422
462
self .log ("Joy timer started to move with joy." )
423
463
@@ -427,41 +467,149 @@ def _buzz(self, intensity: float = 1.0):
427
467
type = JoyFeedback .TYPE_RUMBLE ,
428
468
id = 0 ,
429
469
intensity = intensity ))
470
+
471
+ def _speak (self , msg : str ):
472
+ self ._speak_pub .publish (String (data = msg ))
430
473
474
+ def _controller_callback (self , msg : Joy ):
475
+ if msg .header .stamp .sec == 0 and msg .header .stamp .nanosec == 0 :
476
+ # malformed...
477
+ return
478
+
479
+ now = self .now_stamp
480
+ # Check if the message is older than 0.1 seconds
481
+ msg_age = (now .sec - msg .header .stamp .sec ) + (now .nanosec - msg .header .stamp .nanosec ) * 1e-9
482
+ if msg_age > 0.1 :
483
+ self .log (f"Controller message is older than 0.1s ({ msg_age :.3f} s), ignoring." )
484
+ return
431
485
432
486
433
- def _controller_callback (self , msg : Joy ):
434
487
# right stick = horizontal movement, left stick = vertical movement + yaw
435
488
# like the DJI RC controller
489
+ LH = msg .axes [0 ] # left stick horizontal
490
+ LV = msg .axes [1 ] # left stick vertical
491
+ RH = msg .axes [2 ] # right stick horizontal
492
+ RV = msg .axes [3 ] # right stick vertical
493
+ L2 :float = msg .axes [4 ] # L2 button
494
+ R2 :float = msg .axes [5 ] # R2 button
495
+ south = msg .buttons [0 ] # south button
496
+ east = msg .buttons [1 ] # east button
497
+ west = msg .buttons [2 ] # west button
498
+ north = msg .buttons [3 ] # north button
499
+ select = msg .buttons [4 ] # select button
500
+ ps_button = msg .buttons [5 ] # PS button
501
+ start = msg .buttons [6 ] # start button
502
+ left_stick_in = msg .buttons [7 ] # left stick in
503
+ right_stick_in = msg .buttons [8 ] # right stick in
504
+ L1 = msg .buttons [9 ] # L1 button
505
+ R1 = msg .buttons [10 ] # R1 button
506
+ up = msg .buttons [11 ] # up button
507
+ down = msg .buttons [12 ] # down button
508
+ left = msg .buttons [13 ] # left button
509
+ right = msg .buttons [14 ] # right button
510
+
511
+ sticks_pushed = any (
512
+ [
513
+ abs (LH ) > self ._CONTROLLER_DEADZONE ,
514
+ abs (LV ) > self ._CONTROLLER_DEADZONE ,
515
+ abs (RH ) > self ._CONTROLLER_DEADZONE ,
516
+ abs (RV ) > self ._CONTROLLER_DEADZONE
517
+ ]
518
+ )
519
+
436
520
if not self ._got_control :
437
- self ._buzz ()
438
- return
521
+ if start == 1 and ps_button == 1 :
522
+ self ._take_control ()
523
+ return
524
+
525
+ if sticks_pushed :
526
+ self .log ("Sticks pushed without control!" )
527
+ self ._speak ("You must first take control with the PS button and Start button." )
528
+ self ._buzz ()
529
+ return
530
+
531
+ if self ._move_to_setpoint is not None :
532
+ if sticks_pushed :
533
+ self .log ("Sticks pushed, cancelling move to setpoint." )
534
+ self ._speak ("Move to setpoint cancelled because you touched the controller sticks!" )
535
+ self ._cancel_joy_timer ()
439
536
537
+
538
+ if up :
539
+ self .JOY_PUB_MAX += 0.2
540
+ if self .JOY_PUB_MAX > 5.0 :
541
+ self .JOY_PUB_MAX = 5.0
542
+ self .log (f"Joy max increased to { self .JOY_PUB_MAX :.2f} (m/s)" )
543
+ self ._speak (f"Joy max { self .JOY_PUB_MAX :.1f} " )
544
+ if down :
545
+ self .JOY_PUB_MAX -= 0.2
546
+ if self .JOY_PUB_MAX < 0.0 :
547
+ self .JOY_PUB_MAX = 0.0
548
+ self .log (f"Joy max decreased to { self .JOY_PUB_MAX :.2f} (m/s)" )
549
+ self ._speak (f"Joy max { self .JOY_PUB_MAX :.1f} " )
550
+
551
+
552
+ control_modes = list (ControlModes )
553
+ if left :
554
+ self ._control_mode = control_modes [(control_modes .index (self ._control_mode ) - 1 ) % len (control_modes )]
555
+ self .log (f"Control mode changed to { self ._control_mode .value } " )
556
+ self ._speak (f"Control mode { self ._control_mode .value } " )
557
+ if right :
558
+ self ._control_mode = control_modes [(control_modes .index (self ._control_mode ) + 1 ) % len (control_modes )]
559
+ self .log (f"Control mode changed to { self ._control_mode .value } " )
560
+ self ._speak (f"Control mode { self ._control_mode .value } " )
440
561
441
562
563
+
564
+ if self ._got_control and sticks_pushed :
565
+ joy_msg = Joy ()
566
+ joy_msg .header .stamp = self .now_stamp
567
+ if self ._control_mode == ControlModes .FLUvel :
568
+ # DJI expects Axes: [forward, left, up, yawrate]
569
+ joy_msg .axes = [RV , RH , LV , LH ]
570
+ self ._FLU_vel_joy_pub .publish (joy_msg )
571
+
572
+ elif self ._control_mode == ControlModes .ENUvel :
573
+ self .log ("Moving with ENU velocity control mode, be careful! Right stick is real East/North!" )
574
+ # DJI expects Axes: [east, north, up, yawrate]
575
+ joy_msg .axes = [RV , RH , LV , LH ]
576
+ self ._ENU_vel_joy_pub .publish (joy_msg )
577
+
578
+ elif self ._control_mode == ControlModes .ENUpos :
579
+ #TODO
580
+ pass
442
581
443
- def _move_towards_setpoint_FLUvel (self ):
444
- def cancel_joy_timer ():
445
- if self ._joy_timer is not None :
446
- self ._joy_timer .cancel ()
447
- self ._joy_timer = None
448
- self ._setpoint_received_at = None
449
- self .log ("Joy timer cancelled." )
582
+
583
+
584
+ def _cancel_joy_timer (self ):
585
+ self ._setpoint_received_at = None
586
+ self ._move_to_setpoint = None
587
+ self .log ("Setpoint discarded." )
588
+ if self ._joy_timer is not None :
589
+ self ._joy_timer .cancel ()
590
+ self ._joy_timer = None
591
+ self .log ("Joy timer cancelled." )
450
592
593
+
594
+
595
+
596
+
597
+ def _move_towards_setpoint_FLUvel (self ):
451
598
if self ._move_to_setpoint is None or self ._setpoint_received_at is None :
452
599
self .log ("No move to setpoint set, cannot move with joy." )
600
+ self ._cancel_joy_timer ()
453
601
return
454
602
455
603
if (self .now_stamp .sec - self ._move_to_setpoint .header .stamp .sec ) + \
456
604
(self .now_stamp .nanosec - self ._move_to_setpoint .header .stamp .nanosec ) * 1e-9 > self .MOVE_TO_SETPOINT_MAX_AGE :
457
605
self .log (f"Move to setpoint message is older than { self .MOVE_TO_SETPOINT_MAX_AGE } s, cancelling joy timer." )
458
606
self ._move_to_setpoint = None
459
- cancel_joy_timer ()
607
+ self . _cancel_joy_timer ()
460
608
return
461
609
462
610
if not self ._got_control :
463
611
self .log ("Not got control, cannot move with joy." )
464
- cancel_joy_timer ()
612
+ self . _cancel_joy_timer ()
465
613
return
466
614
467
615
try :
@@ -473,7 +621,7 @@ def cancel_joy_timer():
473
621
target_in_base = do_transform_pose_stamped (self ._move_to_setpoint , tf_diff )
474
622
except :
475
623
self .log (f"Failed to transform move to setpoint from { self ._move_to_setpoint .header .frame_id } to { self .BASE_FLAT_FRAME } , cancelling joy timer." )
476
- cancel_joy_timer ()
624
+ self . _cancel_joy_timer ()
477
625
return
478
626
479
627
@@ -496,9 +644,17 @@ def cancel_joy_timer():
496
644
joy_msg .axes = [e_forw , e_left , e_updn , 0.0 ] # Axes: [forward, left, up/down, yaw]
497
645
joy_msg .buttons = []
498
646
499
- self ._joy_pub .publish (joy_msg )
647
+ self ._FLU_vel_joy_pub .publish (joy_msg )
648
+
500
649
650
+ def move_towards_setpoint_ENUpos (self ):
651
+ self .log ("move_towards_setpoint_ENUpos not implemented yet." )
652
+ self ._cancel_joy_timer ()
501
653
654
+ def move_towards_setpoint_ENUvel (self ):
655
+ self .log ("move_towards_setpoint_ENUvel not implemented yet." )
656
+ self ._cancel_joy_timer ()
657
+
502
658
503
659
def _dji_rc_cb (self , msg : Joy ):
504
660
# if RC is touched by user, we give up control
@@ -545,7 +701,22 @@ def _angular_rate_ground_callback(self, msg: Vector3Stamped):
545
701
def _control_mode_callback (self , msg : ControlMode ):
546
702
# hardcoded numbers from the psdk_ros2 interface
547
703
# 1 = Has control authority, 4 = PSDK
548
- self ._got_control = msg .control_auth == 1 and msg .device_mode == 4
704
+ just_got_control = msg .control_auth == 1 and msg .device_mode == 4
705
+ if self ._got_control == just_got_control :
706
+ return
707
+
708
+ if self ._got_control and not just_got_control :
709
+ self .log ("Released control authority, stopping joy timer, discarding setpoint." )
710
+ self ._speak ("Released control." )
711
+ self ._cancel_joy_timer ()
712
+ self ._got_control = False
713
+
714
+ elif not self ._got_control and just_got_control :
715
+ self .log ("Gained control authority." )
716
+ self ._speak ("Taken control." )
717
+ self ._got_control = True
718
+
719
+
549
720
550
721
def _battery_callback (self , msg : BatteryState ):
551
722
self ._battery_percent = msg .percentage * 100
@@ -590,11 +761,9 @@ def _attitude_callback(self, msg: QuaternionStamped):
590
761
self ._heading_deg = 90 - math .degrees (rpy_enu [2 ])
591
762
self ._base_pose_in_home .pose .orientation = msg .quaternion
592
763
593
- rpy_enu_flat = [0.0 , 0.0 , rpy_enu [2 ]] # we want a frame without roll and pitch for control reasons
594
764
flat_quat = Quaternion ()
595
765
flat_quat .x , flat_quat .y , flat_quat .z , flat_quat .w = quaternion_from_euler (0 , 0 , rpy_enu [2 ])
596
766
self ._base_pose_flat_in_home .pose .orientation = flat_quat
597
- rpy_enu_flat_no_yaw = [0.0 , 0.0 , 0.0 ]
598
767
ENU_quat = Quaternion ()
599
768
ENU_quat .x , ENU_quat .y , ENU_quat .z , ENU_quat .w = quaternion_from_euler (0 , 0 , 0 )
600
769
self ._base_pose_ENU_in_home .pose .orientation = ENU_quat
0 commit comments