#!/usr/bin/env python import os import zmq import json import time import numpy as np from numpy import linalg as LA from threading import Thread from scipy.spatial import cKDTree from selfdrive.swaglog import cloudlog from cereal.services import service_list from common.realtime import Ratekeeper from common.kalman.ned import geodetic2ecef, NED import cereal.messaging as messaging from cereal import log import warnings from selfdrive.config import Conversions as CV if os.getenv('EON_LIVE') == '1': _REMOTE_ADDR = "192.168.5.11" else: _REMOTE_ADDR = "127.0.0.1" LOOP = 'small_loop' TRACK_SNAP_DIST = 17. # snap to a track below this distance TRACK_LOST_DIST = 30. # lose a track above this distance INSTRUCTION_APPROACHING_DIST = 200. INSTRUCTION_ACTIVE_DIST = 20. ROT_CENTER_TO_LOC = 1.2 class INSTRUCTION_STATE: NONE = log.UiNavigationEvent.Status.none PASSIVE = log.UiNavigationEvent.Status.passive APPROACHING = log.UiNavigationEvent.Status.approaching ACTIVE = log.UiNavigationEvent.Status.active def convert_ecef_to_capnp(points): points_capnp = [] for p in points: point = log.ECEFPoint.new_message() point.x, point.y, point.z = map(float, p[0:3]) points_capnp.append(point) return points_capnp def get_spaced_points(track, start_index, cur_ecef, v_ego): active_points = [] look_ahead = 5.0 + 1.5 * v_ego # 5m + 1.5s # forward and backward passes for better poly fit for idx_sign in [1, -1]: for i in range(0, 1000): index = start_index + i * idx_sign # loop around p = track[index % len(track)] distance = LA.norm(cur_ecef - p[0:3]) if i > 5 and distance > look_ahead: break active_points.append([p, index]) # sort points by index active_points = sorted(active_points, key=lambda pt: pt[1]) active_points = [p[0] for p in active_points] return active_points def fit_poly(points, cur_ecef, cur_heading, ned_converter): relative_points = [] for point in points.points: p = np.array([point.x, point.y, point.z]) relative_points.append(ned_converter.ecef_to_ned_matrix.dot(p - cur_ecef)) relative_points = np.matrix(np.vstack(relative_points)) # Calculate relative postions and rotate wrt to heading of car c, s = np.cos(-cur_heading), np.sin(-cur_heading) R = np.array([[c, -s], [s, c]]) n, e = relative_points[:, 0], relative_points[:, 1] relative_points = np.hstack([e, n]) rotated_points = relative_points.dot(R) rotated_points = np.array(rotated_points) x, y = rotated_points[:, 1], -rotated_points[:, 0] warnings.filterwarnings('error') # delete points that go backward max_x = x[0] x_new = [] y_new = [] for xi, yi in zip(x, y): if xi > max_x: max_x = xi x_new.append(xi) y_new.append(yi) x = np.array(x_new) y = np.array(y_new) if len(x) > 10: poly = map(float, np.polyfit(x + ROT_CENTER_TO_LOC, y, 3)) # 1.2m in front else: poly = [0.0, 0.0, 0.0, 0.0] return poly, float(max_x + ROT_CENTER_TO_LOC) def get_closest_track(tracks, track_trees, cur_ecef): track_list = [(name, track_trees[name].query(cur_ecef, 1)) for name in track_trees] closest_name, [closest_distance, closest_idx] = min(track_list, key=lambda x: x[1][0]) return {'name': closest_name, 'distance': closest_distance, 'idx': closest_idx, 'speed': tracks[closest_name][closest_idx][3], 'accel': tracks[closest_name][closest_idx][4]} def get_track_from_name(tracks, track_trees, track_name, cur_ecef): if track_name is None: return None else: track_distance, track_idx = track_trees[track_name].query(cur_ecef, 1) return {'name': track_name, 'distance': track_distance, 'idx': track_idx, 'speed': tracks[track_name][track_idx][3], 'accel': tracks[track_name][track_idx][4]} def get_tracks_from_instruction(tracks,instruction, track_trees, cur_ecef): if instruction is None: return None, None else: source_track = get_track_from_name(tracks, track_trees, instruction['source'], cur_ecef) target_track = get_track_from_name(tracks, track_trees, instruction['target'], cur_ecef) return source_track, target_track def get_next_instruction_distance(track, instruction, cur_ecef): if instruction is None: return None else: return np.linalg.norm(cur_ecef - track[instruction['start_idx']][0:3]) def update_current_track(tracks, cur_track, cur_ecef, track_trees): closest_track = get_closest_track(tracks, track_trees, cur_ecef) # have we lost current track? if cur_track is not None: cur_track = get_track_from_name(tracks, track_trees, cur_track['name'], cur_ecef) if cur_track['distance'] > TRACK_LOST_DIST: cur_track = None # did we snap to a new track? if cur_track is None and closest_track['distance'] < TRACK_SNAP_DIST: cur_track = closest_track return cur_track, closest_track def update_instruction(instruction, instructions, cur_track, source_track, state, cur_ecef, tracks): if state == INSTRUCTION_STATE.ACTIVE: # instruction frozen, just update distance instruction['distance'] = get_next_instruction_distance(tracks[source_track['name']], instruction, cur_ecef) return instruction elif cur_track is None: return None else: instruction_list = [i for i in instructions[cur_track['name']] if i['start_idx'] > cur_track['idx']] if len(instruction_list) > 0: next_instruction = min(instruction_list, key=lambda x: x['start_idx']) next_instruction['distance'] = get_next_instruction_distance(tracks[cur_track['name']], next_instruction, cur_ecef) return next_instruction else: return None def calc_instruction_state(state, cur_track, closest_track, source_track, target_track, instruction): lost_track_or_instruction = cur_track is None or instruction is None if state == INSTRUCTION_STATE.NONE: if lost_track_or_instruction: pass else: state = INSTRUCTION_STATE.PASSIVE elif state == INSTRUCTION_STATE.PASSIVE: if lost_track_or_instruction: state = INSTRUCTION_STATE.NONE elif instruction['distance'] < INSTRUCTION_APPROACHING_DIST: state = INSTRUCTION_STATE.APPROACHING elif state == INSTRUCTION_STATE.APPROACHING: if lost_track_or_instruction: state = INSTRUCTION_STATE.NONE elif instruction['distance'] < INSTRUCTION_ACTIVE_DIST: state = INSTRUCTION_STATE.ACTIVE elif state == INSTRUCTION_STATE.ACTIVE: if lost_track_or_instruction: state = INSTRUCTION_STATE.NONE elif target_track['distance'] < TRACK_SNAP_DIST and \ source_track['idx'] > instruction['start_idx'] and \ instruction['distance'] > 10.: state = INSTRUCTION_STATE.NONE cur_track = target_track return state, cur_track def gps_planner_point_selection(): DECIMATION = 1 cloudlog.info("Starting gps_plannerd point selection") rk = Ratekeeper(10.0, print_delay_threshold=np.inf) context = zmq.Context() live_location = messaging.sub_sock(context, 'liveLocation', conflate=True, addr=_REMOTE_ADDR) car_state = messaging.sub_sock(context, 'carState', conflate=True) gps_planner_points = messaging.pub_sock(context, 'gpsPlannerPoints') ui_navigation_event = messaging.pub_sock(context, 'uiNavigationEvent') # Load tracks and instructions from disk basedir = os.environ['BASEDIR'] tracks = np.load(os.path.join(basedir, 'selfdrive/controls/tracks/%s.npy' % LOOP)).item() instructions = json.loads(open(os.path.join(basedir, 'selfdrive/controls/tracks/instructions_%s.json' % LOOP)).read()) # Put tracks into KD-trees track_trees = {} for name in tracks: tracks[name] = tracks[name][::DECIMATION] track_trees[name] = cKDTree(tracks[name][:,0:3]) # xyz cur_track = None source_track = None target_track = None instruction = None v_ego = 0. state = INSTRUCTION_STATE.NONE counter = 0 while True: counter += 1 ll = messaging.recv_one(live_location) ll = ll.liveLocation cur_ecef = geodetic2ecef((ll.lat, ll.lon, ll.alt)) cs = messaging.recv_one_or_none(car_state) if cs is not None: v_ego = cs.carState.vEgo cur_track, closest_track = update_current_track(tracks, cur_track, cur_ecef, track_trees) #print cur_track instruction = update_instruction(instruction, instructions, cur_track, source_track, state, cur_ecef, tracks) source_track, target_track = get_tracks_from_instruction(tracks, instruction, track_trees, cur_ecef) state, cur_track = calc_instruction_state(state, cur_track, closest_track, source_track, target_track, instruction) active_points = [] # Make list of points used by gpsPlannerPlan if cur_track is not None: active_points = get_spaced_points(tracks[cur_track['name']], cur_track['idx'], cur_ecef, v_ego) cur_pos = log.ECEFPoint.new_message() cur_pos.x, cur_pos.y, cur_pos.z = map(float, cur_ecef) m = messaging.new_message() m.init('gpsPlannerPoints') m.gpsPlannerPoints.curPos = cur_pos m.gpsPlannerPoints.points = convert_ecef_to_capnp(active_points) m.gpsPlannerPoints.valid = len(active_points) > 10 m.gpsPlannerPoints.trackName = "none" if cur_track is None else cur_track['name'] m.gpsPlannerPoints.speedLimit = 100. if cur_track is None else float(cur_track['speed']) m.gpsPlannerPoints.accelTarget = 0. if cur_track is None else float(cur_track['accel']) gps_planner_points.send(m.to_bytes()) m = messaging.new_message() m.init('uiNavigationEvent') m.uiNavigationEvent.status = state m.uiNavigationEvent.type = "none" if instruction is None else instruction['type'] m.uiNavigationEvent.distanceTo = 0. if instruction is None else float(instruction['distance']) endRoadPoint = log.ECEFPoint.new_message() m.uiNavigationEvent.endRoadPoint = endRoadPoint ui_navigation_event.send(m.to_bytes()) rk.keep_time() def gps_planner_plan(): context = zmq.Context() live_location = messaging.sub_sock(context, 'liveLocation', conflate=True, addr=_REMOTE_ADDR) gps_planner_points = messaging.sub_sock(context, 'gpsPlannerPoints', conflate=True) gps_planner_plan = messaging.pub_sock(context, 'gpsPlannerPlan') points = messaging.recv_one(gps_planner_points).gpsPlannerPoints target_speed = 100. * CV.MPH_TO_MS target_accel = 0. last_ecef = np.array([0., 0., 0.]) while True: ll = messaging.recv_one(live_location) ll = ll.liveLocation p = messaging.recv_one_or_none(gps_planner_points) if p is not None: points = p.gpsPlannerPoints target_speed = p.gpsPlannerPoints.speedLimit target_accel = p.gpsPlannerPoints.accelTarget cur_ecef = geodetic2ecef((ll.lat, ll.lon, ll.alt)) # TODO: make NED initialization much faster so we can run this every time step if np.linalg.norm(last_ecef - cur_ecef) > 200.: ned_converter = NED(ll.lat, ll.lon, ll.alt) last_ecef = cur_ecef cur_heading = np.radians(ll.heading) if points.valid: poly, x_lookahead = fit_poly(points, cur_ecef, cur_heading, ned_converter) else: poly, x_lookahead = [0.0, 0.0, 0.0, 0.0], 0. valid = points.valid m = messaging.new_message() m.init('gpsPlannerPlan') m.gpsPlannerPlan.valid = valid m.gpsPlannerPlan.poly = poly m.gpsPlannerPlan.trackName = points.trackName r = [] for p in points.points: point = log.ECEFPoint.new_message() point.x, point.y, point.z = p.x, p.y, p.z r.append(point) m.gpsPlannerPlan.points = r m.gpsPlannerPlan.speed = target_speed m.gpsPlannerPlan.acceleration = target_accel m.gpsPlannerPlan.xLookahead = x_lookahead gps_planner_plan.send(m.to_bytes()) def main(gctx=None): cloudlog.info("Starting gps_plannerd main thread") point_thread = Thread(target=gps_planner_point_selection) point_thread.daemon = True control_thread = Thread(target=gps_planner_plan) control_thread.daemon = True point_thread.start() control_thread.start() while True: time.sleep(1) if __name__ == "__main__": main()