Exemplo n.º 1
0
def dribble(reset=False):
    global wp_counter
    global dist_valid
    
    if reset:
        wp_counter = 0
        dist_valid = False
        
    # dribble active route into the active_node tree (one waypoint
    # per interation to keep the load consistent and light.)
    route_size = len(active_route)
    active_route_node.setInt('route_size', route_size)
    if route_size > 0:
        if wp_counter >= route_size:
            wp_counter = 0
            dist_valid = True
        wp = active_route[wp_counter]
        wp_str = 'wpt[%d]' % wp_counter
        wp_node = active_route_node.getChild(wp_str, True)
        wp_node.setFloat("longitude_deg", wp.lon_deg)
        wp_node.setFloat("latitude_deg", wp.lat_deg)

        if wp_counter < route_size - 1:
            # compute leg course and distance
            next = active_route[wp_counter+1]
            (leg_course, rev_course, leg_dist) = \
                wgs84.geo_inverse( wp.lat_deg, wp.lon_deg,
                                   next.lat_deg, next.lon_deg )
            wp.leg_dist_m = leg_dist
        wp_counter += 1
Exemplo n.º 2
0
def geod2cart(ref, geod_points):
    print('geod2cart()')
    result = []
    for p in geod_points:
        # expects points as [lat, lon]
        #heading, dist = gc.course_and_dist( [ref.y, ref.x], [p.y, p.x] )
        (heading, reverse, dist) = \
            wgs84.geo_inverse( ref.y, ref.x, p.y, p.x )
        angle = (90 - heading) * d2r
        x = math.cos(angle) * dist
        y = math.sin(angle) * dist
        print(p.pretty(), 'hdg:', heading, 'dist:', dist, 'cart:', x, y)
        result.append(point.Point(x, y))
    return result
Exemplo n.º 3
0
    def update(self, dt):
        if not self.active:
            return False
        if not self.home_node.getBool("valid"):
            if self.gps_node.getFloat("gps_age") < 1.0 and \
               self.gps_node.getBool("settle"):
                # Save current position as startup position
                self.startup_node.setFloat(
                    "longitude_deg", self.gps_node.getFloat("longitude_deg"))
                self.startup_node.setFloat(
                    "latitude_deg", self.gps_node.getFloat("latitude_deg"))
                self.startup_node.setFloat(
                    "altitude_m", self.gps_node.getFloat("altitude_m"))
                self.startup_node.setBool("valid", True)

                # Set initial "home" position.
                self.home_node.setFloat(
                    "longitude_deg", self.gps_node.getFloat("longitude_deg"))
                self.home_node.setFloat("latitude_deg",
                                        self.gps_node.getFloat("latitude_deg"))
                self.home_node.setFloat("altitude_m",
                                        self.gps_node.getFloat("altitude_m"))
                self.home_node.setFloat("azimuth_deg", 0.0)
                self.home_node.setBool("valid", True)
        else:
            current = (self.pos_node.getFloat("latitude_deg"),
                       self.pos_node.getFloat("longitude_deg"))
            home = (self.home_node.getFloat("latitude_deg"),
                    self.home_node.getFloat("longitude_deg"))
            (course_deg, rev_deg, dist_m) = \
                wgs84.geo_inverse( self.pos_node.getFloat("latitude_deg"),
                                   self.pos_node.getFloat("longitude_deg"),
                                   self.home_node.getFloat("latitude_deg"),
                                   self.home_node.getFloat("longitude_deg") )
            self.home_node.setFloat("course_deg", course_deg)
            self.home_node.setFloat("dist_m", dist_m)

            # a mini cartesian (2d) system relative to home in meters

            # FIXME: the parametric task is the only thing that uses this
            # so why not put this code over into the parametric.py module?

            theta = rev_deg * d2r
            x = math.sin(theta) * dist_m
            y = math.cos(theta) * dist_m
            self.home_node.setFloat("x_m", x)
            self.home_node.setFloat("y_m", y)
        return True
Exemplo n.º 4
0
        print("dt:", time_str)
        date_time_obj = datetime.datetime.strptime(main_str, '%Y-%m-%d %H:%M:%S')
        unix_sec = float(date_time_obj.strftime('%s')) + fraction
        print("from local:", unix_sec)
        record = djicsv.query(unix_sec)
        roll = record['roll']
        pitch = record['pitch']
        yaw = record['yaw']
        if yaw < 0: yaw += 360.0
    if abs(lat_deg) < 0.001 and abs(lon_deg) < 0.001:
        continue
    
    write_frame = False

    # by distance camera has moved
    (c1, c2, dist_m) = wgs84.geo_inverse(lat_deg, lon_deg, last_lat, last_lon)
    print("dist:", dist_m)
    #if time >= last_time + args.interval and dist_m >= args.distance:
    if args.distance and dist_m >= args.distance:
        write_frame = True
        
    # by visual overlap
    method = cv2.INTER_AREA
    frame_scale = cv2.resize(frame, (0,0), fx=scale, fy=scale,
                             interpolation=method)
    cv2.imshow('frame', frame_scale)
    gray = cv2.cvtColor(frame_scale, cv2.COLOR_BGR2GRAY)
    (h, w) = gray.shape
    kp_list = detector.detect(gray)
    kp_list, des_list = detector.compute(gray, kp_list)
    if not (des_list_ref is None) and not (des_list is None) and len(des_list_ref) and len(des_list):
Exemplo n.º 5
0
def update(dt):
    direction_str = circle_node.getString("direction")
    direction = 1.0
    if direction_str == "right":
        direction = -1.0
    elif direction_str == "left":
        direction = 1.0
    else:
        circle_node.setString("direction", "left")

    if pos_node.hasChild("longitude_deg") and \
         pos_node.hasChild("latitude_deg"):
        pos_lon = pos_node.getFloat("longitude_deg")
        pos_lat = pos_node.getFloat("latitude_deg")
    else:
        # no valid current position, bail out.
        return

    if circle_node.hasChild("longitude_deg") and \
       circle_node.hasChild("latitude_deg"):
        # we have a valid circle center
        center_lon = circle_node.getFloat("longitude_deg")
        center_lat = circle_node.getFloat("latitude_deg")
    else:
        # we have a valid position, but no valid circle center, use
        # current position.  (sanity fallback)
        circle_node.setFloat("longitude_deg", pos_lon)
        circle_node.setFloat("latitude_deg", pos_lat)
        circle_node.setFloat("radius_m", 100.0)
        circle_node.setString("direction", "left")
        center_lon = pos_lon
        center_lat = pos_lat

    # compute course and distance to center of target circle
    # fixme: should reverse this and direction sense to match 'land.py' and make more sense
    (course_deg, rev_deg, dist_m) = \
        wgs84.geo_inverse( pos_lat, pos_lon, center_lat, center_lon )

    # compute ideal ground course to be on the circle perimeter if at
    # ideal radius
    ideal_crs = course_deg + direction * 90
    if ideal_crs > 360.0: ideal_crs -= 360.0
    if ideal_crs < 0.0: ideal_crs += 360.0

    # (in)sanity check
    if circle_node.hasChild("radius_m"):
        radius_m = circle_node.getFloat("radius_m")
        if radius_m < 35: radius_m = 35
    else:
        radius_m = 100
        circle_node.setFloat("radius_m", 100.0)

    # compute a target ground course based on our actual radius
    # distance
    target_crs = ideal_crs
    if dist_m < radius_m:
        # inside circle, adjust target heading to expand our
        # circling radius
        offset_deg = direction * 90.0 * (1.0 - dist_m / radius_m)
        target_crs += offset_deg
    elif dist_m > radius_m:
        # outside circle, adjust target heading to tighten our
        # circling radius
        offset_dist = dist_m - radius_m
        if offset_dist > radius_m: offset_dist = radius_m
        offset_deg = direction * 90 * offset_dist / radius_m
        target_crs -= offset_deg
    if target_crs > 360.0: target_crs -= 360.0
    if target_crs < 0.0: target_crs += 360.0
    targets_node.setFloat("groundtrack_deg", target_crs)

    # L1 'mathematical' response to error
    L1_period = L1_node.getFloat("period")  # gain
    gs_mps = vel_node.getFloat("groundspeed_ms")
    omegaA = sqrt_of_2 * math.pi / L1_period
    VomegaA = gs_mps * omegaA
    course_error = orient_node.getFloat("groundtrack_deg") - target_crs
    # wrap to +/- 180
    if course_error < -180.0: course_error += 360.0
    if course_error > 180.0: course_error -= 360.0
    # clamp to +/-90
    if course_error < -90.0: course_error = -90.0
    if course_error > 90.0: course_error = 90.0
    targets_node.setFloat("course_error_deg", course_error)

    # accel: is the lateral acceleration we need to compensate for
    # heading error
    accel = 2.0 * math.sin(course_error * d2r) * VomegaA

    # circling acceleration needed for our current distance from center
    if dist_m > 0.1:
        turn_accel = direction * gs_mps * gs_mps / dist_m
    else:
        turn_accel = 0.0

    # allow a crude fudge factor for non-straight airframes or imu
    # mounting errors.  This is essentially the bank angle that yields
    # zero turn rate
    bank_bias_deg = L1_node.getFloat("bank_bias_deg")

    # compute desired acceleration = acceleration required for course
    # correction + acceleration required to maintain turn at current
    # distance from center.
    total_accel = accel + turn_accel

    target_bank = -math.atan(total_accel / gravity)
    target_bank_deg = target_bank * r2d + bank_bias_deg

    bank_limit_deg = L1_node.getFloat("bank_limit_deg")
    if target_bank_deg < -bank_limit_deg + bank_bias_deg:
        target_bank_deg = -bank_limit_deg + bank_bias_deg
    if target_bank_deg > bank_limit_deg + bank_bias_deg:
        target_bank_deg = bank_limit_deg + bank_bias_deg

    targets_node.setFloat("roll_deg", target_bank_deg)

    route_node.setFloat("wp_dist_m", dist_m)
    if gs_mps > 0.1:
        route_node.setFloat("wp_eta_sec", dist_m / gs_mps)
    else:
        route_node.setFloat("wp_eta_sec", 0.0)
Exemplo n.º 6
0
def make_pix4d(image_dir, force_altitude=None, force_heading=None, yaw_from_groundtrack=False):
    if not force_altitude and camera.camera_node.getString("make") == "DJI" and camera.camera_node.getString("model") in ["FC330", "FC6310", "FC6310S"]:
        # test for Phantom 4 Pro v2.0 camera which lies about it's altitude
        log("Detected these images are from a Phantom 4 which lies about it's")
        log("altitude.  Please rerun the script with the --force-altitude option to")
        log("override the incorrect goetag altitude with your best estimate of the")
        log("true gps altitude of the flight altitude (in meters).")
        log("Sorry for the inconvenience!")
        quit()
        
    # load list of images
    files = []
    for file in os.listdir(image_dir):
        if fnmatch.fnmatch(file, "*.jpg") or fnmatch.fnmatch(file, "*.JPG"):
            files.append(file)
    files.sort()

    # save some work if true
    images_have_yaw = False

    images = []
    # read image exif timestamp (and convert to unix seconds)
    for file in files:
        full_name = os.path.join(image_dir, file)
        # print(full_name)
        lon_deg, lat_deg, alt_m, unixtime, yaw_deg, pitch_deg, roll_deg = exif.get_pose(full_name)

        line = [file, lat_deg, lon_deg]
        if not force_altitude:
            line.append(alt_m)
        else:
            line.append(force_altitude)

        if roll_deg is None:
            line.append(0)      # assume zero roll
        else:
            line.append(roll_deg)
        if pitch_deg is None:
            line.append(0)      # assume zero pitch
        else:
            line.append(pitch_deg)
        if force_heading is not None:
            line.append(force_heading)
        elif yaw_deg is not None:
            images_have_yaw = True
            line.append(yaw_deg)
        else:
            # no yaw info found in metadata
            line.append(0)
            
        images.append(line)

    if (not force_heading and not images_have_yaw) or yaw_from_groundtrack:
        print("do yaw from ground track")
        # do extra work to estimate yaw heading from gps ground track
        from rcUAS import wgs84
        for i in range(len(images)):
            if i > 0:
                prev = images[i-1]
            else:
                prev = None
            cur = images[i]
            if i < len(images)-1:
                next = images[i+1]
            else:
                next = None

            if not prev is None:
                (prev_hdg, rev_course, prev_dist) = \
                    wgs84.geo_inverse( prev[1], prev[2], cur[1], cur[2] )
            else:
                prev_hdg = 0.0
                prev_dist = 0.0
            if not next is None:
                (next_hdg, rev_course, next_dist) = \
                    wgs84.geo_inverse( cur[1], cur[2], next[1], next[2] )
            else:
                next_hdg = 0.0
                next_dist = 0.0

            prev_hdgx = math.cos(prev_hdg*d2r)
            prev_hdgy = math.sin(prev_hdg*d2r)
            next_hdgx = math.cos(next_hdg*d2r)
            next_hdgy = math.sin(next_hdg*d2r)
            avg_hdgx = (prev_hdgx*prev_dist + next_hdgx*next_dist) / (prev_dist + next_dist)
            avg_hdgy = (prev_hdgy*prev_dist + next_hdgy*next_dist) / (prev_dist + next_dist)
            avg_hdg = math.atan2(avg_hdgy, avg_hdgx)*r2d
            if avg_hdg < 0:
                avg_hdg += 360.0
            #print("%d %.2f %.1f %.2f %.1f %.2f" % (i, prev_hdg, prev_dist, next_hdg, next_dist, avg_hdg))
            images[i][6] = avg_hdg
    
    # sanity check
    output_file = os.path.join(image_dir, "pix4d.csv")
    if os.path.exists(output_file):
        log(output_file, "exists, please rename it and rerun this script.")
        quit()
    log("Creating pix4d image pose file:", output_file, "images:", len(files))
    
    # traverse the image list and create output csv file
    with open(output_file, 'w') as csvfile:
        writer = csv.DictWriter( csvfile,
                                 fieldnames=["File Name",
                                             "Lat (decimal degrees)",
                                             "Lon (decimal degrees)",
                                             "Alt (meters MSL)",
                                             "Roll (decimal degrees)",
                                             "Pitch (decimal degrees)",
                                             "Yaw (decimal degrees)"] )
        writer.writeheader()
        for line in images:
            image = line[0]
            lat_deg = line[1]
            lon_deg = line[2]
            alt_m = line[3]
            roll_deg = line[4]
            pitch_deg = line[5]
            yaw_deg = line[6]
            writer.writerow( { "File Name": os.path.basename(image),
                               "Lat (decimal degrees)": "%.10f" % lat_deg,
                               "Lon (decimal degrees)": "%.10f" % lon_deg,
                               "Alt (meters MSL)": "%.2f" % alt_m,
                               "Roll (decimal degrees)": "%.2f" % roll_deg,
                               "Pitch (decimal degrees)": "%.2f" % pitch_deg,
                               "Yaw (decimal degrees)": "%.2f" % yaw_deg } )
Exemplo n.º 7
0
    def update(self, dt):
        if not self.active:
            return False

        self.glideslope_rad = self.land_node.getFloat("glideslope_deg") * d2r
        self.extend_final_leg_m = self.land_node.getFloat("extend_final_leg_m")
        self.alt_bias_ft = self.land_node.getFloat("altitude_bias_ft")

        # add ability for pilot to bias the glideslope altitude using
        # stick/elevator (negative elevator is up.)
        self.alt_bias_ft += -self.pilot_node.getFloat("elevator") * 25.0

        # compute minimum 'safe' altitude
        safe_dist_m = math.pi * self.turn_radius_m + self.final_leg_m
        safe_alt_ft = safe_dist_m * math.tan(self.glideslope_rad) * m2ft \
                      + self.alt_bias_ft

        # position on circle descent
        circle_pos = 0

        mode = self.nav_node.getString('mode')
        if mode == 'circle':
            # circle descent portion of the approach
            pos_lon = self.pos_node.getFloat("longitude_deg")
            pos_lat = self.pos_node.getFloat("latitude_deg")
            center_lon = self.circle_node.getFloat("longitude_deg")
            center_lat = self.circle_node.getFloat("latitude_deg")
            # compute course and distance to center of target circle
            (course_deg, rev_deg, cur_dist_m) = \
                wgs84.geo_inverse( center_lat, center_lon, pos_lat, pos_lon )
            # test for circle capture
            if not self.circle_capture:
                fraction = abs(cur_dist_m / self.turn_radius_m)
                #print 'heading to circle:', err, fraction
                if fraction > 0.80 and fraction < 1.20:
                    # within 20% of target circle radius, call the
                    # circle capture
                    comms.events.log("land", "descent circle capture")
                    self.circle_capture = True

            # compute portion of circle remaining to tangent point
            current_crs = course_deg + self.side * 90
            if current_crs > 360.0: current_crs -= 360.0
            if current_crs < 0.0: current_crs += 360.0
            circle_pos = (self.final_heading_deg - current_crs) * self.side
            if circle_pos < -180.0: circle_pos += 360.0
            if circle_pos > 180.0: circle_pos -= 360.0
            # print 'circle_pos:', self.orient_node.getFloat('groundtrack_deg'), current_crs, self.final_heading_deg, circle_pos
            angle_rem_rad = math.pi
            if self.circle_capture and circle_pos > -10:
                # circling, captured circle, and within 180 degrees
                # towards tangent point (or just slightly passed)
                angle_rem_rad = circle_pos * math.pi / 180.0

            # distance to edge of circle + remaining circumference of
            # circle + final approach leg
            self.dist_rem_m = (cur_dist_m - self.turn_radius_m) \
                              + angle_rem_rad * self.turn_radius_m \
                              + self.final_leg_m
            # print 'circle:', self.dist_rem_m, self.turn_radius_m, self.final_leg_m, cur_dist_m
            if self.circle_capture and self.gs_capture:
                # we are on the circle and on the glide slope, lets
                # look for our lateral exit point
                if abs(circle_pos) <= 10.0:
                    comms.events.log("land", "transition to final")
                    self.nav_node.setString("mode", "route")
        else:
            # on final approach
            if control.route.dist_valid:
                self.dist_rem_m = self.route_node.getFloat("dist_remaining_m")

        # compute glideslope/target elevation
        alt_m = self.dist_rem_m * math.tan(self.glideslope_rad)
        # print ' ', mode, "dist = %.1f alt = %.1f" % (self.dist_rem_m, alt_m)

        # Compute target altitude.
        cur_alt = self.pos_node.getFloat("altitude_agl_ft")
        cur_target_alt = self.targets_node.getFloat("altitude_agl_ft")
        new_target_alt = alt_m * m2ft + self.alt_bias_ft

        # prior to glide slope capture, never allow target altitude
        # lower than safe altitude
        if not self.gs_capture:
            # print 'safe:', safe_alt_ft, 'new:', new_target_alt
            if new_target_alt < safe_alt_ft:
                new_target_alt = safe_alt_ft

        # We want to avoid wasting energy needlessly gaining altitude.
        # Once the approach has started, never raise the target
        # altitude.
        if new_target_alt > cur_target_alt:
            new_target_alt = cur_target_alt

        self.targets_node.setFloat("altitude_agl_ft", new_target_alt)

        # compute error metrics relative to ideal glide slope
        alt_error_ft = cur_alt - (alt_m * m2ft + self.alt_bias_ft)
        gs_error = math.atan2(alt_error_ft * ft2m, self.dist_rem_m) * r2d
        #print "alt_error_ft = %.1f" % alt_error_ft, "gs err = %.1f" % gs_error

        if self.circle_capture and not self.gs_capture:
            # on the circle, but haven't intercepted gs
            #print 'waiting for gs intercept'
            if gs_error <= 1.0 and circle_pos >= 0:
                # 1 degree or less glide slope error and on the 2nd
                # half of the circle, call the gs captured
                comms.events.log("land", "glide slope capture")
                self.gs_capture = True

        # compute time to touchdown at current ground speed (assuming the
        # navigation system has lined us up properly
        ground_speed_ms = self.vel_node.getFloat("groundspeed_ms")
        if ground_speed_ms > 0.01:
            seconds_to_touchdown = self.dist_rem_m / ground_speed_ms
        else:
            seconds_to_touchdown = 1000.0  # lots

        #print "dist_rem_m = %.1f gs = %.1f secs = %.1f" % \
        #    (self.dist_rem_m, ground_speed_ms, seconds_to_touchdown)

        # approach_speed_kt = approach_speed_node.getFloat()
        self.flare_pitch_deg = self.land_node.getFloat("flare_pitch_deg")
        self.flare_seconds = self.land_node.getFloat("flare_seconds")

        if seconds_to_touchdown <= self.flare_seconds and not self.flare:
            # within x seconds of touchdown horizontally.  Note these
            # are padded numbers because we don't know the truth
            # exactly ... we could easily be closer or lower or
            # further or higher.  Our flare strategy is to smoothly
            # pull throttle to idle, while smoothly pitching to the
            # target flare pitch (as configured in the task
            # definition.)
            comms.events.log("land", "start flare")
            self.flare = True
            self.flare_start_time = self.imu_node.getFloat("timestamp")
            self.approach_throttle = self.engine_node.getFloat("throttle")
            self.approach_pitch = self.targets_node.getFloat("pitch_deg")
            self.flare_pitch_range = self.approach_pitch - self.flare_pitch_deg
            fcsmode.set("basic")

        if self.flare:
            if self.flare_seconds > 0.01:
                elapsed = self.imu_node.getFloat(
                    "timestamp") - self.flare_start_time
                percent = elapsed / self.flare_seconds
                if percent > 1.0:
                    percent = 1.0
                self.targets_node.setFloat(
                    "pitch_deg",
                    self.approach_pitch - percent * self.flare_pitch_range)
                self.engine_node.setFloat(
                    "throttle", self.approach_throttle * (1.0 - percent))
                #printf("FLARE: elapsed=%.1f percent=%.2f speed=%.1f throttle=%.1f",
                #       elapsed, percent,
                #       approach_speed_kt - percent * self.flare_pitch_range,
                #       self.approach_throttle * (1.0 - percent))
            else:
                # printf("FLARE!!!!\n")
                self.targets_node.setFloat("pitch_deg", self.flare_pitch_deg)
                self.engine_node.setFloat("throttle", 0.0)
Exemplo n.º 8
0
def update(dt):
    global current_wp
    global acquired
    
    reposition()

    nav_course = 0.0
    nav_dist_m = 0.0
    direct_dist = 0.0

    request = route_node.getString('route_request')
    if len(request):
        result = ''
        if build_str(request):
            swap()
            reposition(force=True)
            result = 'success: ' + request
            dribble(reset=True)
        else:
            result = 'failed: ' + request
        route_node.setString('request_result', result)
        route_node.setString('route_request', '')

    route_node.setInt('route_size', len(active_route))
    if len(active_route) > 0:
        if gps_node.getFloat("data_age") < 10.0:
            # track current waypoint of route (only!) if we have
            # recent gps data

            # route start up logic: if start_mode == first_wpt
            # then there is nothing to do, we simply continue to
            # track wpt 0 if that is the current waypoint.  If
            # start_mode == 'first_leg', then if we are tracking
            # wpt 0, increment it so we track the 2nd waypoint
            # along the first leg.  If only a 1 point route is
            # given along with first_leg startup behavior, then
            # don't do that again, force some sort of sane route
            # parameters instead!
            start_mode = route_node.getString('start_mode')
            if start_mode == 'first_leg' and current_wp == 0:
                if len(active_route) > 1:
                    current_wp += 1
                else:
                    route_node.setString('start_mode', 'first_wpt')
                    route_node.setString('follow_mode', 'direct')

            L1_period = L1_node.getFloat('period')
            L1_damping = L1_node.getFloat('damping')
            gs_mps = vel_node.getFloat('groundspeed_ms')
            groundtrack_deg = orient_node.getFloat('groundtrack_deg')
            tas_kt = wind_node.getFloat("true_airspeed_kt")
            tas_mps = tas_kt * kt2mps

            prev = get_previous_wp()
            wp = get_current_wp()

            # compute direct-to course and distance
            pos_lon = pos_node.getFloat("longitude_deg")
            pos_lat = pos_node.getFloat("latitude_deg")
            (direct_course, rev_course, direct_dist) = \
                wgs84.geo_inverse( pos_lat, pos_lon, wp.lat_deg, wp.lon_deg )
            #print pos_lat, pos_lon, ":", wp.lat_deg, wp.lon_deg
            #print ' course to:', direct_course, 'dist:', direct_dist

            # compute leg course and distance
            (leg_course, rev_course, leg_dist) = \
                wgs84.geo_inverse( prev.lat_deg, prev.lon_deg,
                                   wp.lat_deg, wp.lon_deg )
            #print prev.lat_deg, prev.lon_deg, " ", wp.lat_deg, wp.lon_deg
            #print ' leg course:', leg_course, 'dist:', leg_dist

            # difference between ideal (leg) course and direct course
            angle = leg_course - direct_course
            if angle < -180.0: angle += 360.0
            elif angle > 180.0: angle -= 360.0

            # compute cross-track error
            angle_rad = angle * d2r
            xtrack_m = math.sin(angle_rad) * direct_dist
            dist_m = math.cos(angle_rad) * direct_dist
            # print("lc: %.1f  dc: %.1f  a: %.1f  xc: %.1f  dd: %.1f" % (leg_course, direct_course, angle, xtrack_m, direct_dist))
            route_node.setFloat( 'xtrack_dist_m', xtrack_m )
            route_node.setFloat( 'projected_dist_m', dist_m )

            # default distance for waypoint acquisition = direct
            # distance to the target waypoint.  This can be
            # overridden later by leg following and replaced with
            # distance remaining along the leg.
            nav_dist_m = direct_dist

            follow_mode = route_node.getString('follow_mode')
            completion_mode = route_node.getString('completion_mode')
            if follow_mode == 'direct':
                # steer direct to
                nav_course = direct_course
            elif follow_mode == 'xtrack_direct_hdg':
                # cross track steering depricated.  See
                # route_mgr.cxx in the historical archives for
                # reference code.
                pass
            elif follow_mode == 'xtrack_leg_hdg':
                # cross track steering depricated.  See
                # route_mgr.cxx in the historical archives for
                # reference code.
                pass
            elif follow_mode == 'leader':
                # scale our L1_dist (something like a target heading
                # gain) proportional to ground speed
                L1_dist = (1.0 / math.pi) * L1_damping * L1_period * gs_mps
                wangle = 0.0
                if L1_dist < 1.0:
                    # ground really small or negative (problem?!?)
                    L1_dist = 1.0
                if L1_dist <= abs(xtrack_m):
                    # beyond L1 distance, steer as directly toward
                    # leg as allowed
                    wangle = 0.0
                else:
                    # steer towards imaginary point projected onto
                    # the route leg L1_distance ahead of us
                    wangle = math.acos(abs(xtrack_m) / L1_dist) * r2d
                if wangle < 30.0: wangle = 30.0
                if xtrack_m > 0.0:
                    nav_course = direct_course + angle - 90.0 + wangle
                else:
                    nav_course = direct_course + angle + 90.0 - wangle
                # print("x: %.1f  dc: %.1f  a: %.1f  wa: %.1f  nc: %.1f" % (xtrack_m, direct_course, angle, wangle, nav_course))
                if acquired:
                    nav_dist_m = dist_m
                else:
                    # direct to first waypoint until we've
                    # acquired this route
                    nav_course = direct_course
                    nav_dist_m = direct_dist

                # printf('direct=%.1f angle=%.1f nav=%.1f L1=%.1f xtrack=%.1f wangle=%.1f nav_dist=%.1f\n', direct_course, angle, nav_course, L1_dist, xtrack_m, wangle, nav_dist_m)

            gs_mps = vel_node.getFloat('groundspeed_ms')
            if gs_mps > 0.1 and abs(nav_dist_m) > 0.1:
                wp_eta_sec = nav_dist_m / gs_mps
            else:
                wp_eta_sec = 99.0 # just any sorta big value
            route_node.setFloat( 'wp_eta_sec', dist_m / gs_mps )
            route_node.setFloat( 'wp_dist_m', direct_dist )
                
            if nav_course < 0.0: nav_course += 360.0
            if nav_course > 360.0: nav_course -= 360.0

            targets_node.setFloat( 'groundtrack_deg', nav_course )

            # allow a crude fudge factor for non-straight airframes or
            # imu mounting errors.  This is essentially the bank angle
            # that yields zero turn rate
            bank_bias_deg = L1_node.getFloat("bank_bias_deg");

            # target bank angle computed here
            target_bank_deg = 0.0

            # heading error is computed with wind triangles so this is
            # the actual body heading error, not the ground track
            # error, thus Vomega is computed with tas_mps, not gs_mps
            omegaA = sqrt_of_2 * math.pi / L1_period
            #VomegaA = gs_mps * omegaA
            #course_error = orient_node.getFloat('groundtrack_deg') \
            #               - nav_course
            VomegaA = tas_mps * omegaA
            # print 'gt:', groundtrack_deg, 'nc:', nav_course, 'error:', groundtrack_deg - nav_course
            hdg_error = wind_heading_error(groundtrack_deg, nav_course)
            # clamp to +/-90 so we still get max turn input when flying directly away from the heading.
            if hdg_error < -90.0: hdg_error = -90.0
            if hdg_error > 90.0: hdg_error = 90.0
            targets_node.setFloat( 'wind_heading_error_deg', hdg_error )

            accel = 2.0 * math.sin(hdg_error * d2r) * VomegaA

            target_bank_deg = -math.atan( accel / gravity )*r2d + bank_bias_deg
            
            bank_limit_deg = L1_node.getFloat('bank_limit_deg')
            if target_bank_deg < -bank_limit_deg + bank_bias_deg:
                target_bank_deg = -bank_limit_deg + bank_bias_deg
            if target_bank_deg > bank_limit_deg + bank_bias_deg:
                target_bank_deg = bank_limit_deg + bank_bias_deg
            targets_node.setFloat( 'roll_deg', target_bank_deg )

            # estimate distance remaining to completion of route
            if dist_valid:
                dist_remaining_m = nav_dist_m + \
                                   get_remaining_distance_from_next_waypoint()
                route_node.setFloat('dist_remaining_m', dist_remaining_m)

            #if comms_node.getBool('display_on'):
            #    print 'next leg: %.1f  to end: %.1f  wpt=%d of %d' % (nav_dist_m, dist_remaining_m, current_wp, len(active_route))

            # logic to mark completion of leg and move to next leg.
            if completion_mode == 'loop':
                if wp_eta_sec < 1.0:
                    acquired = True
                    increment_current_wp()
            elif completion_mode == 'circle_last_wpt':
                if wp_eta_sec < 1.0:
                    acquired = True
                    if current_wp < len(active_route) - 1:
                        increment_current_wp()
                    else:
                        wp = get_current()
                        # FIXME: NEED TO GO TO CIRCLE MODE HERE SOME HOW!!!
                        #mission_mgr.request_task_circle(wp.get_target_lon(),
                        #   wp.get_target_lat(),
                        #   0.0, 0.0)
            elif completion_mode == 'extend_last_leg':
                if wp_eta_sec < 1.0:
                    acquired = True
                    if current_wp < len(active_route) - 1:
                        increment_current_wp()
                    else:
                        # follow the last leg forever
                        pass

            # publish current target waypoint
            route_node.setInt('target_waypoint_idx', current_wp)

            # if ( display_on ) {
            # printf('route dist = %0f\n', dist_remaining_m)
            # }
    else:
        pass
        # FIXME: we've been commanded to follow a route, but no
        # route has been defined.

        # We are in ill-defined territory, should we do some sort
        # of circle of our home position?

        # FIXME: need to go to circle mode somehow here!!!!
        # mission_mgr.request_task_circle()

    # dribble active route into property tree
    dribble()