import sys, curses, math, random, copy
import rclpy
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from geometry_msgs.msg import TwistStamped
from robot_pose import RobotPose, odom2pose, turn_from_to
from nav_msgs.msg import Odometry
from irobot_create_msgs.msg import InterfaceButtons
from grid import GridConverter, UP, DOWN, LEFT, RIGHT, HEADINGS
from fuzzy import fuzzify, defuzzify, f_and, f_or, f_not
ANGLE_TOLERANCE = math.pi/24
DISTANCE_TOLERANCE = 0.05
MAX_FORWARD = 0.25
MAX_TURN = 0.5
MAX_ALIGN = math.pi/8
class GridNode(Node):
def __init__(self, robot_name: str, width: float, height: float, cell_size: float):
super().__init__(f"{robot_name}_GridNode")
self.create_subscription(Odometry, f"{robot_name}/odom", self.odom_callback, qos_profile_sensor_data)
self.motors = self.create_publisher(TwistStamped, f"{robot_name}/cmd_vel_stamped", qos_profile_sensor_data)
self.grid = GridConverter(width, height, cell_size)
self.current_action = None
self.pose = None
self.goal_pose = None
self.last_key = None
def grid_col_row(self) -> tuple[int, int]:
if self.pose is not None:
return self.grid.odom_to_grid(self.pose.point())
def state(self):
col_row = self.grid_col_row()
if col_row is not None:
return self.grid.grid_to_state(col_row)
def odom_callback(self, odom: Odometry):
self.pose = odom2pose(odom)
if self.goal_pose is not None:
self.head_for_goal()
def make_twist(self) -> TwistStamped:
t = TwistStamped()
t.header.frame_id = "base_link"
t.header.stamp = self.get_clock().now().to_msg()
return t
def update_goal(self, action: int):
self.current_action = action
self.goal_pose = copy.copy(self.pose)
self.goal_pose.theta = HEADINGS[self.current_action]
self.goal_pose.move(self.grid.cell_size)
def head_for_goal(self):
target_angle = self.pose.turn_to(self.goal_pose.x, self.goal_pose.y)
distance = self.pose.distance_to(self.goal_pose.x, self.goal_pose.y)
if distance < DISTANCE_TOLERANCE:
self.final_heading_align()
else:
self.go_to_goal(target_angle, distance)
def final_heading_align(self):
offset = turn_from_to(self.pose.theta, self.goal_pose.theta)
if abs(offset) < ANGLE_TOLERANCE:
self.goal_pose = None
else:
t = self.make_twist()
offset_left, offset_right = fuzzy_angle_left_right(offset)
t.twist.angular.z = defuzzy_angle(offset_left, offset_right)
self.motors.publish(t)
def go_to_goal(self, target_angle: float, distance: float):
fuzzy_left, fuzzy_right = fuzzy_angle_left_right(target_angle)
fuzzy_distance = f_and(fuzzify(distance, 0.0, self.grid.cell_size),
f_not(f_or(fuzzy_left, fuzzy_right)))
t = self.make_twist()
t.twist.angular.z = defuzzy_angle(fuzzy_left, fuzzy_right)
t.twist.linear.x = defuzzify(fuzzy_distance, 0.0, MAX_FORWARD)
self.motors.publish(t)
def process_keystroke(self, k: str):
self.last_key = k
if k.isdigit():
self.update_goal(int(k))
def running(self) -> bool:
return self.last_key != 'q'
def display(self, stdscr):
stdscr.addstr(0, 0, "0: up, 1: right, 2: down, 3: left")
if self.last_key is not None:
stdscr.addstr(1, 0, self.last_key)
display_padded(stdscr, 2, f"pose : {self.pose}")
display_padded(stdscr, 3, f"goal_pose: {self.goal_pose}")
display_padded(stdscr, 4, f"action : {self.current_action}")
if self.pose is not None:
display_padded(stdscr, 5, f"state : {self.state()}; ({self.grid_col_row()})")
def display_padded(stdscr, row: int, s: str):
height, width = stdscr.getmaxyx()
stdscr.addstr(row, 0, f"{s}{' ' * (width - len(s) - 1)}")
def fuzzy_angle_left_right(angle: float) -> tuple[float, float]:
fuzzy_left = fuzzify(angle, ANGLE_TOLERANCE, MAX_ALIGN)
fuzzy_right = fuzzify(-angle, ANGLE_TOLERANCE, MAX_ALIGN)
return fuzzy_left, fuzzy_right
def defuzzy_angle(fuzzy_left: float, fuzzy_right: float) -> float:
if fuzzy_left > fuzzy_right:
return defuzzify(fuzzy_left, 0.0, MAX_TURN)
else:
return defuzzify(fuzzy_right, 0.0, -MAX_TURN)
def main(stdscr):
rclpy.init()
node = GridNode(sys.argv[1], float(sys.argv[2]), float(sys.argv[3]), float(sys.argv[4]))
curses.cbreak()
stdscr.nodelay(True)
stdscr.clear()
while node.running():
try:
k = stdscr.getch()
if k != -1:
node.process_keystroke(chr(k))
node.display(stdscr)
rclpy.spin_once(node, timeout_sec=0.0)
except curses.error as e:
if str(e) != 'no input':
stdscr.addstr(0, 0, traceback.format_exc())
rclpy.shutdown()
node.destroy_node()
curses.nocbreak()
curses.echo()
stdscr.refresh()
if __name__ == '__main__':
if len(sys.argv) < 5:
print("Usage: python3 grid_node.py robot_name width height cell_size")
else:
curses.wrapper(main)