From 5ffe89bcbe95abb3ac2dc67c8dcd1206ba570099 Mon Sep 17 00:00:00 2001 From: Jacob Gloudemans Date: Mon, 17 Dec 2018 19:10:07 -0600 Subject: [PATCH 01/10] Added html and css files for the mission control gui - Includes js to dynamically update page by subscribing to ros topics using roslibjs - Also added a python script for sending dummy messages (for testing the gui) - Lots of work still to do for this interface --- .../scripts/drive_motor_controller.py | 2 +- mission_control/resources/css/cc.css | 50 +++ mission_control/resources/html/cc.html | 313 ++++++++++++++++++ mission_control/scripts/test_webpage.py | 54 +++ 4 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 mission_control/resources/css/cc.css create mode 100644 mission_control/resources/html/cc.html create mode 100644 mission_control/scripts/test_webpage.py diff --git a/drive_system/scripts/drive_motor_controller.py b/drive_system/scripts/drive_motor_controller.py index 1bc724e..e11c157 100755 --- a/drive_system/scripts/drive_motor_controller.py +++ b/drive_system/scripts/drive_motor_controller.py @@ -14,7 +14,7 @@ class DriveController: # Constructor def __init__(self): - + # Initialize publishers self.speeds_pub = rospy.Publisher('drive_motor_speeds', UInt8, queue_size=1) diff --git a/mission_control/resources/css/cc.css b/mission_control/resources/css/cc.css new file mode 100644 index 0000000..af66c94 --- /dev/null +++ b/mission_control/resources/css/cc.css @@ -0,0 +1,50 @@ +body { + margin: 0px; +} + +table { + border-collapse: collapse; + width: 100%; +} + +td, th { + border: 1px solid #f0f0f0; + text-align: left; + padding: 8px; + font-size: 14px; +} + +td:nth-child(1) { + width: 150px; +} + +tr:nth-child(even) { + background-color: #f0f0f0; +} + +#left_region { + float: left; + width: 40%; + +} + +#middle_region { + float: left; + width: 30%; +} + +#right_region { + float: left; + width: 30%; +} + +#body_content { + margin-left: auto; + margin-right: auto; + width: 90%; +} + +#header_bar { + background-color: #000000b7; + height: 50px; +} \ No newline at end of file diff --git a/mission_control/resources/html/cc.html b/mission_control/resources/html/cc.html new file mode 100644 index 0000000..fc6b66c --- /dev/null +++ b/mission_control/resources/html/cc.html @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + +
+
+ +
+ +
+ +
+

Left region of page

+
+ +
+
+

General

+ + + + + + + + + + + + + +
Elapsed Time:(No messages receieved)
Battery Level:(No messages receieved)
FSM State:(No messages receieved)
+
+ +
+

Driving & Navigation

+ + + + + + + + + + + + + + + + + +
Estimated Pose:(No messages receieved)
Goal Pose:(No messages receieved)
Estimated Twist:(No messages receieved)
Goal Twist:(No messages receieved)
+
+ +
+

Digging

+ + + + + + + + + + + + + +
Current Dig Cmd:(No messages receieved)
Fill Level:(No messages receieved)
Material Deposited:(No messages receieved)
+
+ +
+

Vision

+ + + + + +
Marker Status:(No messages receieved)
+
+
+
+

Right region of page

+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/mission_control/scripts/test_webpage.py b/mission_control/scripts/test_webpage.py new file mode 100644 index 0000000..98f01c7 --- /dev/null +++ b/mission_control/scripts/test_webpage.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +# ----------------------------- # +# DESCRIPTION OF THIS NODE HERE # +# ----------------------------- # + +# import required packages +import rospy +from geometry_msgs.msg import Twist +from geometry_msgs.msg import Pose +from std_msgs.msg import Bool, Float64, Float32 +from sensor_msgs.msg import Image +from matplotlib import pyplot as plt + + +# Run the node +if __name__ == '__main__': + + # Initialize as ROS node + rospy.init_node('test_webpage') + + # Initialize publishers + elapsed_time_pub = rospy.Publisher("updates/elapsed_time", Float64, queue_size=0) + battery_level_pub = rospy.Publisher("updates/battery_level", Float32, queue_size=0) + robot_pose_pub = rospy.Publisher("updates/robot_pose", Pose, queue_size=0) + + # # Messages to publish # # + + # Elapsed time (Float64) + et_msg = Float64() + et_msg.data = 0.0 + + # Battery level (Float32) + bl_msg = Float32() + bl_msg.data = 12.2 + + # Robot pose (Pose) + rp_msg = Pose() + rp_msg.position.x = 1.2 + rp_msg.position.y = 4.6 + rp_msg.position.z = 8.2 + rp_msg.orientation.x = 0 + rp_msg.orientation.y = 0 + rp_msg.orientation.z = 0 + rp_msg.orientation.w = 1 + + rate = rospy.Rate(1) + while not rospy.is_shutdown(): + + et_msg.data = et_msg.data + 1 + elapsed_time_pub.publish(et_msg) + battery_level_pub.publish(bl_msg) + robot_pose_pub.publish(rp_msg) + rate.sleep() \ No newline at end of file From 19f3a8be5056d17d169ff8fda748ba4fa54c7358 Mon Sep 17 00:00:00 2001 From: Jacob Gloudemans Date: Wed, 19 Dec 2018 00:55:47 -0600 Subject: [PATCH 02/10] Finished most of the generate_images node Node works as intended right now. Additional features may be added in the future if desired but the core functionality is working. Code is clean and well-commented. One change that will eventually need to be made: reading robot dimensions, regions boundaries, image scale, etc. as ROS parameters rather than hard-coding values. --- .gitignore | 3 + mission_control/scripts/generate_images.py | 271 +++++++++++++++++++++ mission_control/scripts/test_webpage.py | 15 +- 3 files changed, 282 insertions(+), 7 deletions(-) create mode 100644 mission_control/scripts/generate_images.py diff --git a/.gitignore b/.gitignore index d194db1..6067920 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Ignore top level CMakeLists.txt CMakeLists.txt +# Ignore vscode hidden folders +/**/*.vscode/ + # Generated by dynamic reconfigure *.cfgc /cfg/cpp/ diff --git a/mission_control/scripts/generate_images.py b/mission_control/scripts/generate_images.py new file mode 100644 index 0000000..27222d6 --- /dev/null +++ b/mission_control/scripts/generate_images.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python + +# ------------------------------------------------------------------------- # +# +# This node listens to updates about the robot's pose, goal pose, and +# velocities and produces image representations of the current state of the +# competition area based on that information. These images are intended to +# be displayed by the 'command center' interface to give operators a visual +# representation of what's happening during the competition. +# +# ------------------------------------------------------------------------- # + +# Import ROS packages +import rospy +from geometry_msgs.msg import Twist, Pose +from sensor_msgs.msg import Image +from std_msgs.msg import Bool +from tf.transformations import euler_from_quaternion, rotation_matrix +from cv_bridge import CvBridge + +# Import other useful packages +from math import sin, cos +import numpy as np +import cv2 + + +# ------------------------------------------------------------------------- # +# +# This function creates an image representing the competition area for the +# mining competition. All relevant dimensions can be passed as parameters +# but defaults are provided to reflect the current state of the rules. The +# scale parameter determines the pixels / meter for the image produced. It +# must be consistent across all drawing functions used or results will be +# incorrect +# +# ------------------------------------------------------------------------- # +def generate_base_img(fh=5.76, fw=3.69, dzl=1.92, ozl=3.84, bh=1.65, bw=0.48, bo=0.50, bc='left', scale=100): + + # Initialize full image + img = np.zeros((int(fh * scale), int(fw * scale), 3), np.uint8) + 244 + + # Add horizontal lines demarcating regions + cv2.line(img, (0, int(dzl * scale)), (int(fw * scale), int(dzl * scale)), (175, 175, 175), 1) + cv2.line(img, (0, int(ozl * scale)), (int(fw * scale), int(ozl * scale)), (175, 175, 175), 1) + + # Calculate corner coords for collection bin + bin_x1 = 0 + bin_y1 = (fh - bo) * scale + bin_y2 = bin_y1 - (bh * scale) + + # Adjust for field configuration + if bc == 'right': + bin_x1 += (fw - bw) * scale + bin_x2 = bin_x1 + (bw * scale) + + # Draw collection bin + cv2.rectangle(img, (int(bin_x1), int(bin_y1)), (int(bin_x2), int(bin_y2)), (255, 231, 198), cv2.FILLED) + cv2.rectangle(img, (int(bin_x1), int(bin_y1)), (int(bin_x2), int(bin_y2)), (160, 122, 69), 2) + + # Return image + return img + + +# ------------------------------------------------------------------------- # +# +# This adds a robot symbol to a provided 'base image' (which can be produced +# with 'generate_base_img'). The symbol includes a box showing the outer +# dimensions of the robot, as well as an arrow to show the current pose of +# the robot. Robot dimensions can be passed as parameters. The colors for +# the robot outline and pose arrow can also be adjusted. Scale determines the +# pixels / meter for the image. +# +# ------------------------------------------------------------------------- # +def draw_robot(base_img, pose, rw=0.6, rl=0.8, r_color=(0,0,0), p_color=(0,150,0), scale=100): + + # Initialize new image from base image + img = np.copy(base_img) + + # Get quaternion from pose message + quat = [pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w] + + # Convert quaternion rotation to rpy, extract yaw + theta = euler_from_quaternion(quat)[2] + + # Draw arrow representing robot pose + arrow_x1 = pose.position.x + arrow_y1 = pose.position.y + arrow_x2 = arrow_x1 + (cos(theta) * .35) + arrow_y2 = arrow_y1 - (sin(theta) * .35) + cv2.arrowedLine(img, (int(arrow_x1 * scale), int(arrow_y1 * scale)), + (int(arrow_x2 * scale), int(arrow_y2 * scale)), p_color, 2, tipLength=0.15) + + # Compute corners of robot before rotation & translation + corner1 = [rl / 2.0, rw / 2.0] + corner2 = [rl / 2.0, -rw / 2.0] + corner3 = [-rl / 2.0, rw / 2.0] + corner4 = [-rl / 2.0, -rw / 2.0] + corners = np.array([corner1, corner2, corner3, corner4]).transpose() + + # Rotation matrix + R = rotation_matrix(theta, (0, 0, 1))[0:2, 0:2] + + # Compute new corner coords + rot_corners = np.matmul(R, corners) + + # Translate to current position + rot_corners[0, :] = rot_corners[0, :] + pose.position.x + rot_corners[1, :] = pose.position.y - rot_corners[1, :] + rot_corners = (rot_corners * scale).astype(np.int32) + + # Draw edges on map + cv2.line(img, (rot_corners[0, 0], rot_corners[1, 0]), (rot_corners[0, 1], rot_corners[1, 1]), r_color, 2) + cv2.line(img, (rot_corners[0, 1], rot_corners[1, 1]), (rot_corners[0, 3], rot_corners[1, 3]), r_color, 2) + cv2.line(img, (rot_corners[0, 0], rot_corners[1, 0]), (rot_corners[0, 2], rot_corners[1, 2]), r_color, 2) + cv2.line(img, (rot_corners[0, 2], rot_corners[1, 2]), (rot_corners[0, 3], rot_corners[1, 3]), r_color, 2) + + # Return adjusted image + return img + + +# ------------------------------------------------------------------------- # +# +# This adds an arc to a provided 'base image' (which can be produced with +# 'generate_base_img') which shows where a robot with the provided pose will +# travel in the next 'tt' (total time) seconds if it continues with the +# specified twist. The granularity of the simulation can be adjusted with the +# 'dt' parameter, as well as the amount of time simulated, with 'tt.' Only +# the x component of the Twist's linear velocity and the z component of its +# angular velocity are considered. Scale determines the pixels / meter of +# the image +# +# ------------------------------------------------------------------------- # +def draw_arc(base_img, pose, twist, dt=0.1, tt=2.0, scale=100): + + # Copy image + img = np.copy(base_img) + + # Get quaternion from pose message + quat = [pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w] + + # Convert quaternion rotation to rpy, extract yaw + theta = euler_from_quaternion(quat)[2] + + # Draw arc based on current robot twist + ct = 0.0 + prev_point = [pose.position.x, pose.position.y] + prev_theta = theta + + while ct < tt: + avg_theta = prev_theta + (0.5 * twist.angular.z * dt) + new_x = prev_point[0] + (twist.linear.x * cos(avg_theta) * dt) + new_y = prev_point[1] - (twist.linear.x * sin(avg_theta) * dt) + + # translated, scaled points for drawing + prev_x_plot = int(prev_point[0] * scale) + prev_y_plot = int(prev_point[1] * scale) + new_x_plot = int(new_x * scale) + new_y_plot = int(new_y * scale) + + # add arc segment to drawing + cv2.line(img, (prev_x_plot, prev_y_plot), (new_x_plot, new_y_plot), (0, 0, 150), 2) + + # Update variables + ct += dt + prev_theta = prev_theta + (avg_theta - prev_theta) * 2 + prev_point = [new_x, new_y] + + # Return the adjusted image + return img + + +# ------------------------------------------------------------------------- # +# +# An object for keeping track of the robot's current state. Subscribers +# should be assigned to the update functions for an object so that the class +# variables of a StateListener object are updated with real value from the +# robot. That way, values will be kept up to date and the latest values can +# be accessed at any time +# +# ------------------------------------------------------------------------- # +class StateListener: + + def __init__(self): + + # Robot initially motionless + init_twist = Twist() + init_twist.linear.x = 0.0 + init_twist.angular.z = 0.0 + + # Variables to store data for the next image + self.robot_pose = None + self.robot_twist = init_twist + self.robot_goal = None + self.obstacles = None + self.marker_status = False + + # Callback function to update robot pose + def update_robot_pose(self, msg): + self.robot_pose = msg + + # Callback function to update robot twist + def update_robot_twist(self, msg): + self.robot_twist = msg + + # Callback function to update robot goal pose + def update_robot_goal(self, msg): + self.robot_goal = msg + + # Callback function to update marker status + def update_marker_status(self, msg): + self.robot_goal = msg.data + + +# ------------------------------------------------------------------------- # +# THIS CODE WILL RUN WHEN THIS NODE IS LAUNCHED. PUBLISHES AN IMAGE SHOWING +# THE CURRENT POSE AND TRAJECTORY OF THE ROBOT, AS WELL AS THE ROBOT'S GOAL +# POSE +# ------------------------------------------------------------------------- # +if __name__ == '__main__': + + # Create handler to store values from updates + data = StateListener() + + # Initialize as ROS node + rospy.init_node('generate_data_vis_images') + + # Subscribe to topics + robot_pose_sub = rospy.Subscriber('updates/robot_pose', Pose, data.update_robot_pose, queue_size=1) + robot_twist_sub = rospy.Subscriber('updates/robot_twist', Twist, data.update_robot_twist, queue_size=1) + robot_goal_sub = rospy.Subscriber('updates/goal_pose', Pose, data.update_robot_goal, queue_size=5) + marker_status_sub = rospy.Subscriber('updates/marker_status', Bool, data.update_marker_status, queue_size=1) + + # Initialize publisher for image frames + img_pub = rospy.Publisher('mc/data_vis_frames', Image, queue_size=1) + + # ROS stuff ready to go + rospy.loginfo("Data Visualization Image Publisher initialized...") + + # Bridge for converting to ROS Image messages + bridge = CvBridge() + + # Create base image + img_scale = 150 + base = generate_base_img(scale=img_scale, bc='left') + + # Loop continuously + rate = rospy.Rate(1) + while not rospy.is_shutdown(): + + # Draw robot pose and arc if any data has been received + if data.robot_pose: + frame = draw_robot(base, data.robot_pose, scale=img_scale) + frame = draw_arc(frame, data.robot_pose, data.robot_twist, scale=img_scale) + else: + frame = np.copy(base) + + # Add goal pose to image if one available + if data.robot_goal: + frame = draw_robot(frame, data.robot_goal, r_color=(200, 200, 100), p_color=(150, 150, 150), scale=img_scale) + + # Convert numpy array to ROS Image message + img_msg = bridge.cv2_to_imgmsg(frame, 'bgr8') + + # Publish message + img_pub.publish(img_msg) + + # Sleep at loop rate + rate.sleep() + + + diff --git a/mission_control/scripts/test_webpage.py b/mission_control/scripts/test_webpage.py index 98f01c7..6e42e04 100644 --- a/mission_control/scripts/test_webpage.py +++ b/mission_control/scripts/test_webpage.py @@ -36,19 +36,20 @@ # Robot pose (Pose) rp_msg = Pose() - rp_msg.position.x = 1.2 - rp_msg.position.y = 4.6 - rp_msg.position.z = 8.2 + rp_msg.position.x = 2.0 + rp_msg.position.y = 1.0 + rp_msg.position.z = 0.0 rp_msg.orientation.x = 0 rp_msg.orientation.y = 0 rp_msg.orientation.z = 0 rp_msg.orientation.w = 1 - rate = rospy.Rate(1) + rate = rospy.Rate(50) + print "here-------------------------------------------------------" while not rospy.is_shutdown(): - et_msg.data = et_msg.data + 1 - elapsed_time_pub.publish(et_msg) - battery_level_pub.publish(bl_msg) + # et_msg.data = et_msg.data + 1 + # elapsed_time_pub.publish(et_msg) + # battery_level_pub.publish(bl_msg) robot_pose_pub.publish(rp_msg) rate.sleep() \ No newline at end of file From 467141b44fe4f42cf032d5f32848373004cceee8 Mon Sep 17 00:00:00 2001 From: Jacob Gloudemans Date: Wed, 19 Dec 2018 15:06:57 -0600 Subject: [PATCH 03/10] Webpage interface now displays image stream -Modified generate_images so it now sends images in compressed .png format -Teleop control features still need to be implemented --- mission_control/resources/css/cc.css | 26 ++- mission_control/resources/html/cc.html | 197 +++++++++++++------- mission_control/resources/image/map_img.bmp | Bin 0 -> 2552886 bytes mission_control/scripts/generate_images.py | 6 +- 4 files changed, 157 insertions(+), 72 deletions(-) create mode 100644 mission_control/resources/image/map_img.bmp diff --git a/mission_control/resources/css/cc.css b/mission_control/resources/css/cc.css index af66c94..822e8c3 100644 --- a/mission_control/resources/css/cc.css +++ b/mission_control/resources/css/cc.css @@ -24,18 +24,31 @@ tr:nth-child(even) { #left_region { float: left; - width: 40%; - + width: auto; + max-width: 40%; + margin-right: 30px; } #middle_region { float: left; - width: 30%; + width: 45%; + margin-right: 30px; +} + +#middle_region_left { + float: left; + width: 48%; + margin-right: 4%; +} + +#middle_region_right { + float: left; + width: 48%; } #right_region { float: left; - width: 30%; + width: 15%; } #body_content { @@ -47,4 +60,9 @@ tr:nth-child(even) { #header_bar { background-color: #000000b7; height: 50px; + margin-bottom: 20px; +} + +#state_image { + max-height: 800px; } \ No newline at end of file diff --git a/mission_control/resources/html/cc.html b/mission_control/resources/html/cc.html index fc6b66c..feafab7 100644 --- a/mission_control/resources/html/cc.html +++ b/mission_control/resources/html/cc.html @@ -34,6 +34,12 @@ // General + var stateImage = new ROSLIB.Topic({ + ros: ros, + name: '/mc/data_vis_frames', + messageType: 'sensor_msgs/CompressedImage' + }); + var elapsedTime = new ROSLIB.Topic({ ros: ros, name: '/updates/elapsed_time', @@ -114,6 +120,11 @@ // General + function stateImageCallback(message) { + var img_obj = document.getElementById("state_image"); + img_obj.setAttribute('src', 'data:image/png;base64,' + message.data); + } + function elapsedTimeCallback(message) { var td_obj = document.getElementById("data_elapsed_time"); td_obj.textContent = message.data; @@ -195,6 +206,7 @@ elapsedTime.subscribe(elapsedTimeCallback); fsmState.subscribe(fsmStateCallback); batteryLevel.subscribe(batteryLevelCallback); + stateImage.subscribe(stateImageCallback); // Driving & Navigation robotPose.subscribe(robotPoseCallback); @@ -227,80 +239,135 @@
-

Left region of page

+
-
-

General

- - - - - - - - - - - - - -
Elapsed Time:(No messages receieved)
Battery Level:(No messages receieved)
FSM State:(No messages receieved)
-
- -
-

Driving & Navigation

- - - - - - - - - - - - - - - - - -
Estimated Pose:(No messages receieved)
Goal Pose:(No messages receieved)
Estimated Twist:(No messages receieved)
Goal Twist:(No messages receieved)
-
-
-

Digging

- - - - - - - - - - - - - -
Current Dig Cmd:(No messages receieved)
Fill Level:(No messages receieved)
Material Deposited:(No messages receieved)
+
+ +
+

General

+ + + + + + + + + + + + + +
Elapsed Time:(No messages receieved)
Battery Level:(No messages receieved)
FSM State:(No messages receieved)
+
+ +
+

Driving & Navigation

+ + + + + + + + + + + + + + + + + +
Estimated Pose:(No messages receieved)
Goal Pose:(No messages receieved)
Estimated Twist:(No messages receieved)
Goal Twist:(No messages receieved)
+
+ +
+

Digging

+ + + + + + + + + + + + + +
Current Dig Cmd:(No messages receieved)
Fill Level:(No messages receieved)
Material Deposited:(No messages receieved)
+
+ +
+

Vision

+ + + + + +
Marker Status:(No messages receieved)
+
-
-

Vision

- - - - - -
Marker Status:(No messages receieved)
+
+ +
+

Other Category 1

+ + + + + + + + + + + + + + + + + + + + + +
other_data_1:(No messages receieved)
other_data_2:(No messages receieved)
other_data_3:(No messages receieved)
other_data_4:(No messages receieved)
other_data_5:(No messages receieved)
+
+ +
+

Other Category 2

+ + + + + + + + + + + + + + + + + +
other_data_6:(No messages receieved)
other_data_7:(No messages receieved)
other_data_8:(No messages receieved)
other_data_9:(No messages receieved)
+
-

Right region of page

+

Right region of page

diff --git a/mission_control/resources/image/map_img.bmp b/mission_control/resources/image/map_img.bmp new file mode 100644 index 0000000000000000000000000000000000000000..c0945a0cb1f3f4738bdbac2e8ba2e9cc6bff96d3 GIT binary patch literal 2552886 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 zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; zCZMN;1!i0V1PBlyP$ZzI6oqY70t5&UAP^>?r-TJ&Tml3L5Fk(_pr;gtZB+sU2oN9; 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 z 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 Date: Thu, 10 Jan 2019 18:39:28 -0600 Subject: [PATCH 04/10] Added JS function to mission_control html file This function calls increases twist linear velocity values if the up arrow key is pressed and decreases values if the up arrow key is not pressed --- mission_control/resources/html/cc.html | 47 ++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/mission_control/resources/html/cc.html b/mission_control/resources/html/cc.html index feafab7..a5aadbd 100644 --- a/mission_control/resources/html/cc.html +++ b/mission_control/resources/html/cc.html @@ -85,6 +85,53 @@ messageType: 'geometry_msgs/Twist' }); + //TODO In progress + var lin_vel_x = 0; + var lin_vel_y = 0; + var lin_vel_z = 0; + + var isUpKeyPressed = false; + var keydown = function(e){ + if(e.keyCode == 38){ + isUpKeyPressed = true; + } + } + var keyup = function(e){ + isUpKeyPressed = false; + } + + this.addEventListener("keydown", keydown,false); + this.addEventListener("keyup", keyup,false); + + var twist = new ROSLIB.message({ + linear : { + x : lin_vel_x, + y : lin_vel_y, + z : lin_vel_z + }, + angular : { + x : 0, + y : 0, + z : 0 + } + }); + + setInterval(function(){ + if(isDownKeyPressed){ + ++lin_vel_x; + ++lin_vel_y; + ++lin_vel_z; + } else if (!isDownKeyPressed) { + if (lin_vel_x > 0) + --lin_vel_x; + if (lin_vel_y > 0) + --lin_vel_y; + if (lin_vel_z > 0) + --lin_vel_z; + } + goalTwist.publish(twist); + },100) + // Digging Info From 4baa29c4778004560eb18fa0d6b4c06ec0790a0b Mon Sep 17 00:00:00 2001 From: Jason Lao Date: Thu, 10 Jan 2019 19:05:43 -0600 Subject: [PATCH 05/10] Debugged newly added JS function --- mission_control/resources/html/cc.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mission_control/resources/html/cc.html b/mission_control/resources/html/cc.html index a5aadbd..d3133d4 100644 --- a/mission_control/resources/html/cc.html +++ b/mission_control/resources/html/cc.html @@ -103,7 +103,7 @@ this.addEventListener("keydown", keydown,false); this.addEventListener("keyup", keyup,false); - var twist = new ROSLIB.message({ + var twist = new ROSLIB.Message({ linear : { x : lin_vel_x, y : lin_vel_y, @@ -117,11 +117,11 @@ }); setInterval(function(){ - if(isDownKeyPressed){ + if(isUpKeyPressed){ ++lin_vel_x; ++lin_vel_y; ++lin_vel_z; - } else if (!isDownKeyPressed) { + } else if (!isUpKeyPressed) { if (lin_vel_x > 0) --lin_vel_x; if (lin_vel_y > 0) From 6b38d3b78f91cae3365e3677a45789e97b781aac Mon Sep 17 00:00:00 2001 From: Jason Lao Date: Thu, 24 Jan 2019 20:11:18 -0600 Subject: [PATCH 06/10] WIP #4: Created skeleton node file for teleop control --- high_level_control/scripts/teleop_control.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 high_level_control/scripts/teleop_control.py diff --git a/high_level_control/scripts/teleop_control.py b/high_level_control/scripts/teleop_control.py new file mode 100644 index 0000000..9b46f6a --- /dev/null +++ b/high_level_control/scripts/teleop_control.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python + +# ----------------------------- # +# DESCRIPTION OF THIS NODE HERE # +# ----------------------------- # + +# import required packages +import rospy +from geometry_msgs.msg import Twist +from sensor_msgs.msg import Joy + +class TeleopController: + #stuff + + +if __name__ = '__main__': + #stuff \ No newline at end of file From af7557a2fea47691975b3ff4b08b6071d7c64a3b Mon Sep 17 00:00:00 2001 From: Jason Lao Date: Sat, 26 Jan 2019 16:21:09 -0600 Subject: [PATCH 07/10] WIP #4: Created publisher, subscriber, and callback function for drive --- high_level_control/scripts/teleop_control.py | 34 ++++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/high_level_control/scripts/teleop_control.py b/high_level_control/scripts/teleop_control.py index 9b46f6a..1fe72f7 100644 --- a/high_level_control/scripts/teleop_control.py +++ b/high_level_control/scripts/teleop_control.py @@ -9,9 +9,37 @@ from geometry_msgs.msg import Twist from sensor_msgs.msg import Joy -class TeleopController: - #stuff +class TeleopControl: + + # Constructor + def __init__(self): + + # Initialize publishers + self.drive_pub = rospy.Publisher('drive_cmd', Twist) + + # Initialize subscribers + self.joy_sub = rospy.Subscriber("joy", Joy, self.joy_callback) + + + # Callback function for joystick controls + def joy_callback(self, data): + twist = Twist() + twist.linear.x = data.axes[1] + twist.angular.z = data.axes[0] + self.drive_pub.publish(twist) if __name__ = '__main__': - #stuff \ No newline at end of file + + # Initialize as ROS node + rospy.init_node('teleop_control') + + # Create a TeleopControl object + control = TeleopControl() + + # Ready to go + rospy.loginfo("Teleop Control initialized...") + + # Loop continuously + while not rospy.is_shutdown(): + pass \ No newline at end of file From e9e68a682e50b871ab0a0194da6190c090c80d11 Mon Sep 17 00:00:00 2001 From: Jacob Gloudemans Date: Tue, 29 Jan 2019 16:54:39 -0600 Subject: [PATCH 08/10] WIP #4: Added dependencies to package.xml and CMakeLists Added geometry_msgs and sensor_msgs as dependencies Also fixed a minor bug (== vs =) --- high_level_control/CMakeLists.txt | 2 ++ high_level_control/package.xml | 2 ++ high_level_control/scripts/teleop_control.py | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/high_level_control/CMakeLists.txt b/high_level_control/CMakeLists.txt index c5d1227..bebe815 100644 --- a/high_level_control/CMakeLists.txt +++ b/high_level_control/CMakeLists.txt @@ -11,6 +11,8 @@ find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs + geometry_msgs + sensor_msgs ) ## System dependencies are found with CMake's conventions diff --git a/high_level_control/package.xml b/high_level_control/package.xml index fabcd73..fc6d5d8 100644 --- a/high_level_control/package.xml +++ b/high_level_control/package.xml @@ -24,5 +24,7 @@ roscpp rospy std_msgs + sensor_msgs + geometry_msgs diff --git a/high_level_control/scripts/teleop_control.py b/high_level_control/scripts/teleop_control.py index 1fe72f7..b94ac8f 100644 --- a/high_level_control/scripts/teleop_control.py +++ b/high_level_control/scripts/teleop_control.py @@ -29,7 +29,7 @@ def joy_callback(self, data): self.drive_pub.publish(twist) -if __name__ = '__main__': +if __name__ == '__main__': # Initialize as ROS node rospy.init_node('teleop_control') From 74d05bd1ff0d0529785c9a087cfeec15e943423d Mon Sep 17 00:00:00 2001 From: Jason Lao Date: Thu, 31 Jan 2019 19:13:36 -0600 Subject: [PATCH 09/10] Added comments clarifying on joy.buttons and joy.axes --- high_level_control/scripts/teleop_control.py | 31 ++++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/high_level_control/scripts/teleop_control.py b/high_level_control/scripts/teleop_control.py index 1fe72f7..b3d01f1 100644 --- a/high_level_control/scripts/teleop_control.py +++ b/high_level_control/scripts/teleop_control.py @@ -1,8 +1,33 @@ #!/usr/bin/env python -# ----------------------------- # -# DESCRIPTION OF THIS NODE HERE # -# ----------------------------- # +# ----------------------------------------------- # +# Table of index number of /joy.buttons: +# Index Button name on the actual controller +# +# 0 A +# 1 B +# 2 X +# 3 Y +# 4 LB +# 5 RB +# 6 back +# 7 start +# 8 power +# 9 Button stick left +# 10 Button stick right + +# Table of index number of /joy.axes: +# Index Axis name on the actual controller +# +# 0 Moving left joystick left (+) and right (-) changes rotational velocity +# 1 Moving left joystick up (+) and down (-) changes linear velocity +# 2 LT +# 3 Left/Right Axis stick right +# 4 Up/Down Axis stick right +# 5 RT +# 6 cross key left/right +# 7 cross key up/down +# ----------------------------------------------- # # import required packages import rospy From 457e0e8ba7205f7d8e5d9b950554d11680eb8214 Mon Sep 17 00:00:00 2001 From: Jason Lao Date: Thu, 31 Jan 2019 19:50:24 -0600 Subject: [PATCH 10/10] WIP #4: Adjusted callback to call various sub functions This small change allows us to easily add additional callback sub-functions as we eventually assign more buttons to their respective control --- high_level_control/scripts/teleop_control.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/high_level_control/scripts/teleop_control.py b/high_level_control/scripts/teleop_control.py index 8ed6fc8..41196d4 100644 --- a/high_level_control/scripts/teleop_control.py +++ b/high_level_control/scripts/teleop_control.py @@ -41,6 +41,7 @@ def __init__(self): # Initialize publishers self.drive_pub = rospy.Publisher('drive_cmd', Twist) + # other publishers will be added when necessary # Initialize subscribers self.joy_sub = rospy.Subscriber("joy", Joy, self.joy_callback) @@ -48,11 +49,22 @@ def __init__(self): # Callback function for joystick controls def joy_callback(self, data): + self.set_drive_speed(data) + self.set_digging_mode(data) + + # Sets drive speed based on left joystick input + def set_drive_speed(self, data): twist = Twist() twist.linear.x = data.axes[1] twist.angular.z = data.axes[0] self.drive_pub.publish(twist) + # Sets digging mode based on ???? (TBD) + def set_digging_mode(self, data): + pass + + # additional functions TBD + if __name__ == '__main__':