Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Odrive dev cleanup #135

Merged
merged 31 commits into from
Nov 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0ffe4e9
First commit
eozturk2 Sep 4, 2023
160e56d
Adjusted steering twist
eozturk2 Sep 4, 2023
b3e6d47
rad/s to rev/s conversion implemented
eozturk2 Sep 4, 2023
a9a3ea5
Removed dead code
eozturk2 Sep 4, 2023
5000edd
Small refactoring
eozturk2 Sep 4, 2023
c8f9bd1
Big refactor, decluttered the main file
eozturk2 Sep 4, 2023
e7f634d
Added documentation to finished functions
eozturk2 Sep 5, 2023
e3e7255
Fixed missing part in doc
eozturk2 Sep 5, 2023
1fae234
More docs
eozturk2 Sep 5, 2023
edccebc
Logic error
eozturk2 Sep 5, 2023
d6fe5da
Generalized enumeration to find multiple ODrives
eozturk2 Sep 5, 2023
d334057
Typo
eozturk2 Sep 5, 2023
a72dfe3
Added simple keyboard control + file changes for Jetson
eozturk2 Sep 7, 2023
f1e17e4
Implemented error handling, need to test
eozturk2 Sep 9, 2023
436b1c1
Fixed buildpath
eozturk2 Sep 9, 2023
eb42ae0
Quick fix
eozturk2 Sep 11, 2023
25b0110
Bug fix
eozturk2 Sep 11, 2023
18ad9ec
Merge branch 'odrive-dev' of https://github.com/mcgill-robotics/rover…
eozturk2 Sep 11, 2023
cab08e0
Fix?
eozturk2 Sep 11, 2023
8f14c1b
Fix.
eozturk2 Sep 11, 2023
a35ace4
Fix 2
eozturk2 Sep 11, 2023
720ce60
Stuff
eozturk2 Sep 11, 2023
dc656bf
More stuff
eozturk2 Sep 11, 2023
194e229
More stuff
eozturk2 Sep 11, 2023
6f6541a
Fix?
eozturk2 Sep 11, 2023
00fde7b
Fix.
eozturk2 Sep 11, 2023
8f6ef08
All four drive motors working
eozturk2 Sep 28, 2023
5c893e3
Moved util function from ODrive to ODrive_utils.py
David-Ly-3990 Oct 21, 2023
18135f3
Merge branch 'main' into odrive-dev-cleanup
David-Ly-3990 Nov 7, 2023
f020e7f
Update drive_control_node.py
David-Ly-3990 Nov 9, 2023
e4b6c09
removed vs-code workspace file
David-Ly-3990 Nov 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion drive_control/src/drive_control_node.py
David-Ly-3990 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ def run(self):
# Populate the message with the averaged values.
cmd.left[0], cmd.left[1] = correct_lf, correct_lb
cmd.right[0], cmd.right[1] = correct_rf, correct_rb

print(cmd)

self.angular_velocity_publisher.publish(cmd) # Send the angular speeds.

self.rate.sleep()
Expand All @@ -99,4 +102,4 @@ def run(self):
# ROS runtime main entry point
if __name__ == "__main__":
driver = Node_DriveControl()
rospy.spin()
rospy.spin()
58 changes: 53 additions & 5 deletions human_control_interface/src/Controller/GamepadProcess.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import rospy
from pynput import keyboard
from human_control_interface.msg import Gamepad_input
from camera_data.msg import Camera_Orientation
from geometry_msgs.msg import Twist
Expand Down Expand Up @@ -27,7 +28,17 @@ def __init__(self, v_max, w_max):
rospy.init_node("gamepad_process_node")

# Initialize a Gamepad object
self.gamepad = Gamepad()
self.gamepad_init_successful = False
try:
self.gamepad = Gamepad()
self.gamepad_init_successful = True
except:
print("Controller not found. Falling back to debug keyboard control")
self.listener = keyboard.Listener(on_press=self.keyboardProcessCall)
self.keyboard_accumulator_linear = 0.0
self.keyboard_accumulator_twist = 0.0
self.keyboard_sensitivity = 0.05
self.listener.start()


# initialize variables for velocity
Expand All @@ -50,18 +61,21 @@ def __init__(self, v_max, w_max):

self.run()



# The run loop that updates a controller's value.
def run(self):
while not rospy.is_shutdown():
if rospy.is_shutdown():
exit()
try:

# Skip the rest of the loop if gamepad is not connected
if not self.gamepad_init_successful:
continue

self.gamepad.update()
msg = Gamepad_input()

# Transfer Data into msg
# Transfer Data into msg
msg.B1 = self.gamepad.data.b1
msg.B2 = self.gamepad.data.b2
msg.B3 = self.gamepad.data.b3
Expand Down Expand Up @@ -90,6 +104,40 @@ def run(self):

exit()

def keyboardProcessCall(self, key):
if key == keyboard.Key.up:
# self.roverLinearVelocity = 10
self.keyboard_accumulator_linear += self.keyboard_sensitivity
if key == keyboard.Key.down:
# self.roverLinearVelocity = -10
self.keyboard_accumulator_linear -= self.keyboard_sensitivity
if key == keyboard.Key.left:
self.keyboard_accumulator_twist -= self.keyboard_sensitivity
if key == keyboard.Key.right:
self.keyboard_accumulator_twist += self.keyboard_sensitivity
if key == keyboard.KeyCode.from_char('0'):
self.keyboard_accumulator_linear = 0.0
self.keyboard_accumulator_twist = 0.0

if self.keyboard_accumulator_linear > 1.0:
self.keyboard_accumulator_linear = 1.0
elif self.keyboard_accumulator_linear < -1.0:
self.keyboard_accumulator_linear = -1.0

if self.keyboard_accumulator_twist > 1.0:
self.keyboard_accumulator_twist = 1.0
elif self.keyboard_accumulator_twist < -1.0:
self.keyboard_accumulator_twist= -1.0
self.roverLinearVelocity = self.maxLinearVelocity * self.keyboard_accumulator_linear
self.roverAngularVelocity = self.maxAngularVelocity * self.keyboard_accumulator_twist

roverTwist = Twist()
roverTwist.linear.x = self.roverLinearVelocity
roverTwist.angular.z = self.roverAngularVelocity

#time.sleep(0.5)
self.drive_publisher.publish(roverTwist)

# Poll the gamepad data and then call the respective process call.
def gamepadProcessCall(self, msg):
self.driveProcessCall(msg)
Expand Down Expand Up @@ -148,5 +196,5 @@ def risingEdge(self, prevSignal, nextSignal):


if __name__ == "__main__":
gamepadProcess = Node_GamepadProcessing(1, 3)
gamepadProcess = Node_GamepadProcessing(10, 10)
#rospy.spin()
43 changes: 43 additions & 0 deletions odrive_interface/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
cmake_minimum_required(VERSION 3.0.2)
project(odrive_interface)



## Find catkin macros and libraries
## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz)
## is used, also find other catkin packages
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)

catkin_python_setup()

## Generate messages in the 'msg' folder
add_message_files(
FILES
MotorError.msg
MotorState.msg
)

generate_messages(
DEPENDENCIES
std_msgs
)

catkin_package(
CATKIN_DEPENDS
roscpp
rospy
std_msgs
LIBRARIES odrive_interface
CATKIN_DEPENDS message_runtime
)

catkin_install_python(PROGRAMS
src/ODrive.py

DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
2 changes: 2 additions & 0 deletions odrive_interface/msg/MotorError.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
string id
string error
2 changes: 2 additions & 0 deletions odrive_interface/msg/MotorState.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
string id
string state
34 changes: 34 additions & 0 deletions odrive_interface/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0"?>
<package format="2">
<name>odrive_interface</name>
<version>0.0.0</version>
<description>Package that interfaces with the ODrives</description>

<!-- One maintainer tag required, multiple allowed, one person per tag -->
<!-- Example: -->
<!-- <maintainer email="[email protected]">Jane Doe</maintainer> -->
<maintainer email="[email protected]">Eren Ozturk</maintainer>


<license>MIT</license>

<author email="[email protected]">Eren Ozturk</author>

<build_depend>message_generation</build_depend>
<build_export_depend>message_generation</build_export_depend>
<exec_depend>message_runtime</exec_depend>
<buildtool_depend>catkin</buildtool_depend>
<build_depend>roscpp</build_depend>
<build_depend>rospy</build_depend>
<build_depend>std_msgs</build_depend>
<build_export_depend>roscpp</build_export_depend>
<build_export_depend>rospy</build_export_depend>
<build_export_depend>std_msgs</build_export_depend>
<exec_depend>roscpp</exec_depend>
<exec_depend>rospy</exec_depend>
<exec_depend>std_msgs</exec_depend>

<export>

</export>
</package>
12 changes: 12 additions & 0 deletions odrive_interface/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## ! DO NOT MANUALLY INVOKE THIS setup.py, USE CATKIN INSTEAD

from distutils.core import setup
from catkin_pkg.python_setup import generate_distutils_setup

# fetch values from package.xml
setup_args = generate_distutils_setup(
packages=['odrive_interface'],
package_dir={'': 'src'},
)

setup(**setup_args)
154 changes: 154 additions & 0 deletions odrive_interface/src/ODrive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import os, sys
currentdir = os.path.dirname(os.path.realpath(__file__))
sys.path.append(currentdir)

# TODO: Figure out why catkin on the Jetson isn't playing nice with this import. It worked on my PC -Eren
# import init_functions
import rospy
from drive_control.msg import WheelSpeed
from odrive_interface.msg import MotorError, MotorState
from odrive_interface.src.ODrive_utils import *
from odrive.enums import AxisState, ProcedureResult

# TODO: Once drive is working well, expand this node to include the three arm motors
class Node_ODriveInterface():
def __init__(self):
self.drive_lb = None
self.drive_rb = None
self.lb_speed_cmd = 0.0
self.rb_speed_cmd = 0.0
self.lf_speed_cmd = 0.0
self.rf_speed_cmd = 0.0

self.active_errors = {"DRIVE_LB": 0, "DRIVE_LF": 0, "DRIVE_RB": 0, "DRIVE_RF": 0}

# Subscriptions
rospy.init_node('odrive_interface')
self.feedback_publisher = rospy.Publisher("/wheel_velocity_feedback", WheelSpeed, queue_size=1)
self.error_publisher = rospy.Publisher("/odrive_error", MotorError, queue_size=1)
self.state_publisher = rospy.Publisher("/odrive_state", MotorState, queue_size=1)
self.command_subscriber = rospy.Subscriber("/wheel_velocity_cmd", WheelSpeed, self.handle_drive_command)
# TODO: Determine what metacommands software want to use
# self.metacommand_subscriber = rospy.Subscriber("/odrive_meta_cmd")


# Frequency of the ODrive I/O
self.rate = rospy.Rate(100)
self.run()

def handle_drive_command(self, command):
self.lb_speed_cmd = command.left[0] * 0.159155
self.lf_speed_cmd = command.left[1] * 0.159155
self.rb_speed_cmd = command.right[0] * 0.159155
self.rf_speed_cmd = command.right[1] * 0.159155


def run(self):
# Discover motors on startup and assign variables for each
motors_dict = enumerate_motors()
if not motors_dict:
raise IOError("Motor enumeration failed")

drive_lb = motors_dict["DRIVE_LB"]
drive_lf = motors_dict["DRIVE_LF"]
drive_rb = motors_dict["DRIVE_RB"]
drive_rf = motors_dict["DRIVE_RF"]
drive_motors = [drive_lb, drive_lf, drive_rb, drive_rf]

# Calibrate all motors
if not calibrate_motors(drive_motors):
raise IOError("Motor initialization failed")

while not rospy.is_shutdown():
# Put in speed command
drive_lb.axis0.controller.input_vel = -self.lb_speed_cmd
drive_lf.axis0.controller.input_vel = -self.lf_speed_cmd
drive_rb.axis0.controller.input_vel = self.rb_speed_cmd
drive_rf.axis0.controller.input_vel = self.rf_speed_cmd

# Get feedback and publish it to "/wheel_velocity_feedback"
feedback = WheelSpeed()
measured_speed_lb = -drive_lb.encoder_estimator0.vel_estimate
measured_speed_lf = -drive_lf.encoder_estimator0.vel_estimate
measured_speed_rb = drive_rb.encoder_estimator0.vel_estimate
measured_speed_rf = drive_rf.encoder_estimator0.vel_estimate

feedback.left[0], feedback.left[1] = measured_speed_lb, measured_speed_lf
feedback.right[0], feedback.right[1] = measured_speed_rb, measured_speed_rf
self.feedback_publisher.publish(feedback)

print(f"\rDRIVE_LB: {round(measured_speed_lb, 2)}, DRIVE_LF: {round(measured_speed_lf, 2)}, \
DRIVE_RB: {round(measured_speed_rb, 2)}, DRIVE_RF: {round(measured_speed_rf, 2)}", end='')

# Poll the ODrives for their states and check for errors
for motor in drive_motors:
state_fb = MotorState()
state_fb.id = drive_ids[format(motor.serial_number, "x").upper()]

# TODO: Decode and add state information
# state_fb.state =
self.state_publisher.publish(state_fb)

if motor.axis0.active_errors != 0:
# Tell the rover to stop
for motor in drive_motors:
motor.axis0.controller.input_vel = 0

# Wait until it actually stops
motor_stopped = False
while not motor_stopped:
motor_stopped = True
for motor in drive_motors:
if abs(motor.encoder_estimator0.vel_estimate) >= 0.01:
motor_stopped = False
if rospy.is_shutdown():
print("Shutdown prompt received. Setting all motors to idle state.")
for motor in drive_motors:
motor.axis0.requested_state = AxisState.IDLE
break

# Wait for two seconds while all the transient currents and voltages calm down
rospy.sleep(5)

# Now try to recover from the error. This will always succeed the first time, but if
# the error persists, the ODrive will not allow the transition to closed loop control, and
# re-throw the error.
motor.clear_errors()
rospy.sleep(0.5)
motor.axis0.requested_state = AxisState.CLOSED_LOOP_CONTROL
rospy.sleep(0.5)

# If the motor cannot recover successfully publish a message about the error, then print to console
if motor.axis0.active_errors != 0:
error_fb = MotorError()
error_fb.id = drive_ids[format(motor.serial_number, "x").upper()]
error_fb.error = decode_errors(motor.axis0.active_errors)
self.error_publisher.publish(error_fb)
print(f"\nError(s) occurred. Motor ID: {error_fb.id}, Error(s): {error_fb.error}")

# Finally, hang the node and keep trying to recover until the error is gone or the shutdown signal is received
print(f"\nMotor {error_fb.id} could not recover from error(s) {error_fb.error}. R to retry, keyboard interrupt to shut down node.")
while not rospy.is_shutdown():
prompt = input(">").upper()
if prompt == "R":
motor.clear_errors()
rospy.sleep(0.5)
motor.axis0.requested_state = AxisState.CLOSED_LOOP_CONTROL
rospy.sleep(0.5)
if motor.axis0.active_errors == 0:
break
else:
print("Recovery failed. Try again?")


self.rate.sleep()

# On shutdown, bring motors to idle state
print("Shutdown prompt received. Setting all motors to idle state.")
for motor in drive_motors:
motor.axis0.requested_state = AxisState.IDLE


if __name__ == "__main__":
driver = Node_ODriveInterface()
rospy.spin()
Loading
Loading