lib/sgs/navigate.rb in sgslib-1.5.1 vs lib/sgs/navigate.rb in sgslib-1.6.0

- old
+ new

@@ -29,109 +29,233 @@ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # ABSTRACT +# All of the code to navigate a sailboat to a series of waypoints is defined +# herein. The main Navigate class does not save anything to Redis, it +# is purely a utility class for navigation. The navigation is based on my +# paper "An Attractor/Repellor Approach to Autonomous Sailboat Navigation". +# https://link.springer.com/chapter/10.1007/978-3-319-72739-4_6 # +# We save a copy of the actual mission so we can find the attractors and +# repellors. We also assume that doing a GPS.load will pull the latest +# GPS co-ordinates and an Otto.load will pull the latest telemetry from +# the boat. Specifically, the GPS will give us our lat/long and the Otto +# data will allow us to compute the actual wind direction (as well as the +# boat heading and apparent wind angle). +# ## # module SGS class Navigate - attr_reader :mode + # + # Initialize the navigational parameters + def initialize(mission) + @mission = mission + @swing = 45 + end - MODE_SLEEP = 0 - MODE_TEST = 1 - MODE_MANUAL = 2 - MODE_UPDOWN = 3 - MODE_OLYMPIC = 4 - MODE_PRE_MISSION = 5 - MODE_MISSION = 6 - MODE_MISSION_END = 7 - MODE_MISSION_ABORT = 8 - - MODENAMES = [ - "Sleeping...", - "Test Mode", - "Manual Steering", - "Sail Up and Down", - "Sail a Triangle", - "Pre-Mission Wait", - "On Mission", - "Mission Ended", - "** Mission Abort **" - ].freeze - - def initialize - @mode = MODE_SLEEP - @waypoint = nil - @curpos = nil - super + # + # Compute the best heading based on our current position and the position + # of the current attractor. This is where the heavy-lifting happens + def navigate + if @mission.status.current_waypoint == -1 + @mission.status.current_waypoint = 0 + @mission.status.distance = 0 + end + set_waypoint + puts "Attempting to navigate to #{@waypoint}..." + # + # Pull the latest GPS data... + @gps = GPS.load + puts "GPS: #{@gps}" + return unless @gps.valid? + # + # Pull the latest Otto data... + @otto = Otto.load + puts "OTTO:" + p @otto + puts "Compass: #{@otto.compass}" + puts "AWA: #{@otto.awa}" + puts "Wind: #{@otto.wind}" + # + # Update our local copy of the course based on what Otto says. + puts "Course:" + @course = Course.new + @course.heading = @otto.compass + @course.awa = @otto.awa + @course.compute_wind + # + # Compute a new course from the parameter set + compute_new_course end # - # Main daemon function (called from executable) - def self.daemon - puts "Navigation system starting up..." + # Compute a new course based on our position and other information. + def compute_new_course + puts "Compute new course..." # - # Load the mission data from Redis and augment it with the - # contents of the mission file. - config = SGS::Config.load - mission = SGS::Mission.file_load config.mission_file + # First off, compute distance and bearing from our current location + # to every attractor and repellor. We only look at forward attractors, + # not ones behind us. + compute_bearings(@mission.attractors[@mission.status.current_waypoint..-1]) + compute_bearings(@mission.repellors) # - # Now listen for GPS data... - SGS::GPS.subscribe do |count| - puts "Received new GPS count: #{count}" - case SGS::MissionStatus.state - when STATE_COMPASS_FOLLOW - when STATE_WIND_FOLLOW - mission.navigate - when STATE_COMPLETE - when STATE_TERMINATED - when STATE_FAILURE - mission.hold_station + # Right. Now look to see if we've achieved the current waypoint and + # adjust, accordingly + while active? and reached? + next_waypoint! + end + return nil unless active? + puts "Angle to next waypoint: #{@waypoint.bearing.angle_d}d" + puts "Adjusted distance to waypoint is #{@waypoint.distance}" + # + # Now, start the vector field analysis by examining headings either side + # of the bearing to the waypoint. + best_course = @course + best_relvmg = 0.0 + puts "Currently on a #{@course.tack_name} tack (heading is #{@course.heading_d} degrees)" + (-@swing..@swing).each do |alpha_d| + new_course = Course.new(@course.wind) + new_course.heading = waypoint.bearing.angle + Bearing.dtor(alpha_d) + # + # Ignore head-to-wind cases, as they're pointless. When looking at + # the list of waypoints to compute relative VMG, only look to the next + # three or so waypoints. + next if new_course.speed < 0.001 + relvmg = 0.0 + relvmg = new_course.relative_vmg(@mission.attractors[@mission.status.current_waypoint]) + end_wpt = @mission.status.current_waypoint + 3 + if end_wpt >= @mission.attractors.count + end_wpt = @mission.attractors.count - 1 end - gps = SGS::GPS.load - p gps + @mission.attractors[@mission.status.current_waypoint..end_wpt].each do |waypt| + relvmg += new_course.relative_vmg(waypt) + end + @mission.repellors.each do |waypt| + relvmg -= new_course.relative_vmg(waypt) + end + relvmg *= 0.1 if new_course.tack != @course.tack + if relvmg > best_relvmg + best_relvmg = relvmg + best_course = new_course + end end + if best_course.tack != @course.tack + puts "TACKING!!!!" + end + best_course end # - # What is the mode name? - def mode_name - MODENAMES[@mode] + # Compute the bearing for every attractor or repellor + def compute_bearings(waypoints) + waypoints.each do |waypt| + waypt.compute_bearing(@gps.location) + end end - def mode=(val) - puts "SETTING NEW MODE TO #{MODENAMES[val]}" - @mode = val + # + # Set new position + def set_position(time, loc) + @where = loc + @time = time + @track << TrackPoint.new(@time, @where) end # - # This is the main navigator function. It does several things; - # 1. Look for the next waypoint and compute bearing and distance to it - # 2. Decide if we have reached the waypoint (and adjust accordingly) - # 3. Compute the boat heading (and adjust accordingly) - def run - puts "Navigator mode is #{mode_name}: Current Position:" - p curpos - p waypoint - case @mode - when MODE_UPDOWN - upwind_downwind_course - when MODE_OLYMPIC - olympic_course - when MODE_MISSION - mission - when MODE_MISSION_END - mission_end - when MODE_MISSION_ABORT - mission_abort + # Advance the mission by a number of seconds (computing the new location + # in the process). Fake out the speed and thus the location. + def simulated_movement(how_long = 60) + puts "Advancing mission by #{how_long}s" + distance = @course.speed * how_long.to_f / 3600.0 + puts "Travelled #{distance * 1852.0} metres in that time." + set_position(@time + how_long, @where + Bearing.new(@course.heading, distance)) + end + + # + # How long has the mission been active? + def elapsed + @time - @start_time + end + + # + # Check we're active - basically, are there any more waypoints left? + def active? + @mission.status.current_waypoint < @mission.attractors.count + end + + # + # Have we reached the waypoint? Note that even though the waypoints have + # a "reached" circle, we discard the last 10m on the basis that it is + # within the GPS error. + def reached? + puts "ARE WE THERE YET? (dist=#{@waypoint.distance})" + p @waypoint + return true if @waypoint.distance <= 0.0054 + # + # Check to see if the next WPT is nearer than the current one + #if current_wpt < (@mission.attractors.count - 1) + # next_wpt = @mission.attractors[@current_wpt + 1] + # brng = @mission.attractors[@current_wpt].location - next_wpt.location + # angle = Bearing.absolute(waypoint.bearing.angle - next_wpt.bearing.angle) + # return true if brng.distance > next_wpt.distance and + # angle > (0.25 * Math::PI) and + # angle < (0.75 * Math::PI) + #end + puts "... Sadly, no." + return false + end + + # + # Advance to the next waypoint. Return TRUE if + # there actually is one... + def next_waypoint! + @mission.status.current_waypoint += 1 + puts "Attempting to navigate to new waypoint: #{waypoint}" + set_waypoint + end + + # + # Set the waypoint instance variable based on where we are + def set_waypoint + @waypoint = @mission.attractors[@mission.status.current_waypoint] + end + + # + # Return the mission status as a string + def status_str + mins = elapsed / 60 + hours = mins / 60 + mins %= 60 + days = hours / 24 + hours %= 24 + str = ">>> #{@time}, " + if days < 1 + str += "%dh%02dm" % [hours, mins] + else + str += "+%dd%%02dh%02dm" % [days, hours, mins] end + str + ": My position is #{@where}" end # + # Compute the remaining distance from the current location + def overall_distance + dist = 0.0 + loc = @where + @mission.attractors[@mission.status.current_waypoint..-1].each do |wpt| + wpt.compute_bearing(loc) + dist += wpt.bearing.distance + loc = wpt.location + end + dist + end + + # # Navigate a course up to a windward mark which is one nautical mile # upwind of the start position. From there, navigate downwind to the # finish position def upwind_downwind_course end @@ -162,15 +286,15 @@ end # # What is our current position? def curpos - @curpos ||= SGS::GPS.load + @curpos ||= GPS.load end # # What is the next waypoint? def waypoint - @waypoint ||= SGS::Waypoint.load + @waypoint ||= Waypoint.load end end end