Skip to content

Commit 03d6f2d

Browse files
committed
djicapt bringup dds discovery server + djicapt joystick interaction
1 parent 1e5f496 commit 03d6f2d

File tree

2 files changed

+210
-40
lines changed

2 files changed

+210
-40
lines changed

scripts/smarc_bringups/scripts/dji_bringup.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ if [ "$USE_SIM_TIME" = "False" ]; then
4949
tmux send-keys "ros2 topic echo /$ROBOT_NAME/captain_status std_msgs/msg/String --field data" C-m
5050

5151
tmux select-pane -t $SESSION:0.3
52-
tmux send-keys "cd ~ && ./record_bag_ex_camComp.sh" C-m
52+
# tmux send-keys "cd ~ && ./record_bag_ex_camComp.sh" C-m
53+
tmux send-keys "fast-discovery-server -i 0" C-m
5354
else
5455
tmux select-window -t $SESSION:0
5556
tmux send-keys "ros2 topic pub /$ROBOT_NAME/smarc/vehicle_health std_msgs/msg/Int8 \"data: 0\" -r 5" C-m

vehicles/hardware/dji/dji_captain/dji_captain/dji_captain.py

Lines changed: 208 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,22 @@ class DjiCaptain():
6969
def __init__(self, node: Node):
7070
self._node = node
7171
self._node.declare_parameter("robot_name", "M350")
72+
self._node.declare_parameter("controller_deadzone", 0.1)
7273

7374

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
7580

7681

7782
self._control_mode = ControlModes.FLUvel
7883
self._move_to_setpoint : PoseStamped | None = None
7984
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)
8188
self._setpoint_received_at : float|None = None
8289

8390
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):
253260
self._land_srv = node.create_client(Trigger, PSDKTopics.LAND_SRV.value)
254261

255262

256-
while True:
263+
while rclpy.ok():
257264
commands = "Commands:\n"
258265
commands += " 1: Take control \n"
259266
commands += " 2: Release control\n"
260267
commands += " 3: Take off\n"
261268
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"
264270
commands += " 9: Set max joy to (DANGEROUS, DONT USE UNLESS YOUR NAME STARTS WITH O)\n"
271+
commands += " 0: EXIT \n"
265272
try:
266273
self.log(commands)
267274
n = int(input("Enter number for command: \n"))
268275
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()
276277
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()
284279
elif n == 3: #Take off
285280
if self._got_control is False:
286281
self.log("You must take control first!")
@@ -315,9 +310,13 @@ def __init__(self, node: Node):
315310
self.log(self.status_str)
316311
elif n == 9: # set max joy
317312
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)")
319314
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.")
321320
break
322321

323322
except:
@@ -374,6 +373,41 @@ def log(self, msg: str):
374373
self._node.get_logger().info(msg)
375374

376375

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+
377411

378412
def _geo_alt_cb(self, msg: Float32):
379413
self._geo_altitude = msg.data
@@ -417,7 +451,13 @@ def _move_to_setpoint_callback(self, msg: PoseStamped):
417451
self.log(f"Move to setpoint received: {format_pose_stamped(self._move_to_setpoint)}")
418452

419453
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+
421461
self._setpoint_received_at = time.time()
422462
self.log("Joy timer started to move with joy.")
423463

@@ -427,41 +467,149 @@ def _buzz(self, intensity: float = 1.0):
427467
type=JoyFeedback.TYPE_RUMBLE,
428468
id=0,
429469
intensity=intensity))
470+
471+
def _speak(self, msg: str):
472+
self._speak_pub.publish(String(data=msg))
430473

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
431485

432486

433-
def _controller_callback(self, msg: Joy):
434487
# right stick = horizontal movement, left stick = vertical movement + yaw
435488
# 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+
436520
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()
439536

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}")
440561

441562

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
442581

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.")
450592

593+
594+
595+
596+
597+
def _move_towards_setpoint_FLUvel(self):
451598
if self._move_to_setpoint is None or self._setpoint_received_at is None:
452599
self.log("No move to setpoint set, cannot move with joy.")
600+
self._cancel_joy_timer()
453601
return
454602

455603
if (self.now_stamp.sec - self._move_to_setpoint.header.stamp.sec) + \
456604
(self.now_stamp.nanosec - self._move_to_setpoint.header.stamp.nanosec) * 1e-9 > self.MOVE_TO_SETPOINT_MAX_AGE:
457605
self.log(f"Move to setpoint message is older than {self.MOVE_TO_SETPOINT_MAX_AGE}s, cancelling joy timer.")
458606
self._move_to_setpoint = None
459-
cancel_joy_timer()
607+
self._cancel_joy_timer()
460608
return
461609

462610
if not self._got_control:
463611
self.log("Not got control, cannot move with joy.")
464-
cancel_joy_timer()
612+
self._cancel_joy_timer()
465613
return
466614

467615
try:
@@ -473,7 +621,7 @@ def cancel_joy_timer():
473621
target_in_base = do_transform_pose_stamped(self._move_to_setpoint, tf_diff)
474622
except:
475623
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()
477625
return
478626

479627

@@ -496,9 +644,17 @@ def cancel_joy_timer():
496644
joy_msg.axes = [e_forw, e_left, e_updn, 0.0] # Axes: [forward, left, up/down, yaw]
497645
joy_msg.buttons = []
498646

499-
self._joy_pub.publish(joy_msg)
647+
self._FLU_vel_joy_pub.publish(joy_msg)
648+
500649

650+
def move_towards_setpoint_ENUpos(self):
651+
self.log("move_towards_setpoint_ENUpos not implemented yet.")
652+
self._cancel_joy_timer()
501653

654+
def move_towards_setpoint_ENUvel(self):
655+
self.log("move_towards_setpoint_ENUvel not implemented yet.")
656+
self._cancel_joy_timer()
657+
502658

503659
def _dji_rc_cb(self, msg: Joy):
504660
# if RC is touched by user, we give up control
@@ -545,7 +701,22 @@ def _angular_rate_ground_callback(self, msg: Vector3Stamped):
545701
def _control_mode_callback(self, msg: ControlMode):
546702
# hardcoded numbers from the psdk_ros2 interface
547703
# 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+
549720

550721
def _battery_callback(self, msg: BatteryState):
551722
self._battery_percent = msg.percentage*100
@@ -590,11 +761,9 @@ def _attitude_callback(self, msg: QuaternionStamped):
590761
self._heading_deg = 90 - math.degrees(rpy_enu[2])
591762
self._base_pose_in_home.pose.orientation = msg.quaternion
592763

593-
rpy_enu_flat = [0.0, 0.0, rpy_enu[2]] # we want a frame without roll and pitch for control reasons
594764
flat_quat = Quaternion()
595765
flat_quat.x, flat_quat.y, flat_quat.z, flat_quat.w = quaternion_from_euler(0, 0, rpy_enu[2])
596766
self._base_pose_flat_in_home.pose.orientation = flat_quat
597-
rpy_enu_flat_no_yaw = [0.0, 0.0, 0.0]
598767
ENU_quat = Quaternion()
599768
ENU_quat.x, ENU_quat.y, ENU_quat.z, ENU_quat.w = quaternion_from_euler(0, 0, 0)
600769
self._base_pose_ENU_in_home.pose.orientation = ENU_quat

0 commit comments

Comments
 (0)