sgslib 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +7 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/sgs/alarm.rb +103 -0
- data/lib/sgs/command.rb +167 -0
- data/lib/sgs/gps.rb +53 -0
- data/lib/sgs/location.rb +328 -0
- data/lib/sgs/logger.rb +96 -0
- data/lib/sgs/mission.rb +362 -0
- data/lib/sgs/navigate.rb +141 -0
- data/lib/sgs/nmea.rb +138 -0
- data/lib/sgs/otto.rb +163 -0
- data/lib/sgs/polar.rb +86 -0
- data/lib/sgs/redis_base.rb +251 -0
- data/lib/sgs/setup.rb +40 -0
- data/lib/sgs/timing.rb +45 -0
- data/lib/sgs/version.rb +3 -0
- data/lib/sgs/waypoint.rb +115 -0
- data/lib/sgslib.rb +46 -0
- data/sgslib.gemspec +35 -0
- metadata +129 -0
data/lib/sgs/mission.rb
ADDED
@@ -0,0 +1,362 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2013, Kalopa Research. All rights reserved. This is free
|
3
|
+
# software; you can redistribute it and/or modify it under the terms of the
|
4
|
+
# GNU General Public License as published by the Free Software Foundation;
|
5
|
+
# either version 2, or (at your option) any later version.
|
6
|
+
#
|
7
|
+
# It is distributed in the hope that it will be useful, but WITHOUT
|
8
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
9
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
10
|
+
# for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License along
|
13
|
+
# with this product; see the file COPYING. If not, write to the Free
|
14
|
+
# Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
|
15
|
+
#
|
16
|
+
# THIS SOFTWARE IS PROVIDED BY KALOPA RESEARCH "AS IS" AND ANY EXPRESS OR
|
17
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
18
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
19
|
+
# IN NO EVENT SHALL KALOPA RESEARCH BE LIABLE FOR ANY DIRECT, INDIRECT,
|
20
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
21
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
22
|
+
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
23
|
+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
24
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
25
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
26
|
+
#
|
27
|
+
|
28
|
+
##
|
29
|
+
# Routines for handling sailboat navigation and route planning.
|
30
|
+
#
|
31
|
+
require 'date'
|
32
|
+
|
33
|
+
module SGS
|
34
|
+
#
|
35
|
+
# Handle a specific mission.
|
36
|
+
class Mission
|
37
|
+
attr_accessor :id, :where, :time, :start_time
|
38
|
+
attr_accessor :course, :heading, :twa, :vmg
|
39
|
+
attr_accessor :waypoints, :repellors, :track
|
40
|
+
attr_accessor :root_dir
|
41
|
+
|
42
|
+
#
|
43
|
+
# Create the attractors and repellors
|
44
|
+
def initialize(id = 0)
|
45
|
+
@root_dir = "."
|
46
|
+
@waypoints = []
|
47
|
+
@repellors = []
|
48
|
+
@track = []
|
49
|
+
@id = id
|
50
|
+
@time = DateTime.now
|
51
|
+
@wpt_index = -1
|
52
|
+
@heading = -1
|
53
|
+
@course = -1
|
54
|
+
@tack = 0
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Load a new mission from the missions directory.
|
59
|
+
def self.load(id)
|
60
|
+
mission = new
|
61
|
+
mission.id = id
|
62
|
+
mission.read(File.open(mission.filename))
|
63
|
+
mission
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Parse a mission file.
|
68
|
+
def read(file)
|
69
|
+
file.each do |line|
|
70
|
+
unless line =~ /^#/
|
71
|
+
args = line.split(':')
|
72
|
+
code = args[0]
|
73
|
+
loc = Location.parse_str(args[1])
|
74
|
+
vec = Bearing.degrees(args[2], args[3])
|
75
|
+
name = args[4].strip
|
76
|
+
case code
|
77
|
+
when /\d/
|
78
|
+
@waypoints[code.to_i] = Waypoint.new(loc, vec, name)
|
79
|
+
when /[Xx]/
|
80
|
+
@where = loc
|
81
|
+
@wind = vec
|
82
|
+
compute_heading if active?
|
83
|
+
when /[Rr]/
|
84
|
+
@repellors << Waypoint.new(loc, vec, name, Waypoint::REPELLOR)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
@wpt_index = -1
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Save the mission.
|
93
|
+
def save
|
94
|
+
write(File.open(filename, 'w'))
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# Write a mission to a file.
|
99
|
+
def write(file)
|
100
|
+
file.puts "#\n# My starting position."
|
101
|
+
file.puts "X:%s:0:12:Starting position" % @where
|
102
|
+
file.puts "#\n# Waypoints."
|
103
|
+
@waypoints.each_with_index do |wpt, i|
|
104
|
+
file.puts "%d:%s:%d:%f:%s" % [i,
|
105
|
+
wpt.location,
|
106
|
+
wpt.chord.angle_degrees,
|
107
|
+
wpt.chord.distance,
|
108
|
+
wpt.name]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
# Commence a mission...
|
114
|
+
def commence(time = nil)
|
115
|
+
@start_time = time || DateTime.now
|
116
|
+
@time = @start_time
|
117
|
+
@wpt_index = 0
|
118
|
+
@have_new_waypoint = true
|
119
|
+
@have_new_heading = true
|
120
|
+
@have_new_tack = false
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Terminate a mission.
|
125
|
+
def terminate
|
126
|
+
@wpt_index = -1
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# On-mission means we have something to do. In other words, we have a
|
131
|
+
# waypoint to get to.
|
132
|
+
def active?
|
133
|
+
@wpt_index >= 0 and @wpt_index < @waypoints.count
|
134
|
+
end
|
135
|
+
|
136
|
+
#
|
137
|
+
# Set our current location (and timestamp).
|
138
|
+
def set_location(where, time = nil)
|
139
|
+
@where = where
|
140
|
+
@time = time || DateTime.now
|
141
|
+
#
|
142
|
+
# Update the track with our new position, and find the next waypoint.
|
143
|
+
@track << Track.new(@time, @where.clone)
|
144
|
+
again = false
|
145
|
+
begin
|
146
|
+
wpt = @waypoints[@wpt_index]
|
147
|
+
puts ">>Waypoint is #{wpt}"
|
148
|
+
wpt.compute_bearing(@where)
|
149
|
+
puts ">>Course #{wpt.bearing}"
|
150
|
+
if wpt.in_scope?
|
151
|
+
puts "We're at the mark. Time to find the next waypoint..."
|
152
|
+
@wpt_index += 1
|
153
|
+
@have_new_waypoint = again = true
|
154
|
+
else
|
155
|
+
again = false
|
156
|
+
end
|
157
|
+
end while again and active?
|
158
|
+
#
|
159
|
+
# OK, now compute our heading...
|
160
|
+
if active?
|
161
|
+
newc = wpt.bearing.angle_degrees
|
162
|
+
if newc != @course
|
163
|
+
puts "New course: #{newc}"
|
164
|
+
@course = newc
|
165
|
+
compute_heading
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
#
|
170
|
+
# Compute the most effective TWA/VMG for the course.
|
171
|
+
# Because I tend to forget, here's a run-down of the terminology...
|
172
|
+
# @course is where we're trying to get to. It is our compass bearing to the
|
173
|
+
# next mark or finish or whatever. @heading is the compass bearing we're
|
174
|
+
# going to sail to, which may not be the same as our course (we can't sail
|
175
|
+
# upwind, for example). alpha is the difference between our heading and the
|
176
|
+
# course. @twa is the true wind angle, relative to the front of the boat.
|
177
|
+
# Negative values mean a port tack, positive values are starboard. @vmg is
|
178
|
+
# the velocity made-good. In other words, the speed at which we're heading
|
179
|
+
# to the mark. It's defined as Vcos(alpha) where V is the velocity through
|
180
|
+
# the water.
|
181
|
+
def compute_heading
|
182
|
+
puts "Computing Heading and TWA..."
|
183
|
+
wad = @wind.angle_degrees.to_i
|
184
|
+
puts "Wind angle:%03dd" % wad
|
185
|
+
puts "Required course:%03dd" % @course
|
186
|
+
polar = Polar.new
|
187
|
+
@twa ||= 0
|
188
|
+
#
|
189
|
+
# Try an alpha angle between -60 and +60 degrees either side of the
|
190
|
+
# course. The point of this exercise is to compute the TWA for that
|
191
|
+
# heading, compute the speed at that trial TWA, and then compute
|
192
|
+
# the VMG for it. We compute the maximum VMG (and TWA) and use that
|
193
|
+
# to drive the boat. The PVMG is the poisoned version of the VMG,
|
194
|
+
# adjusted so that the opposite tack is less-favoured.
|
195
|
+
ideal_twa = 0
|
196
|
+
ideal_vmg = 0.0
|
197
|
+
max_pvmg = 0.0
|
198
|
+
curr_tack = @twa < 0.0 ? -1 : 1
|
199
|
+
puts "Current tack is %d" % curr_tack
|
200
|
+
(-60..60).each do |alpha|
|
201
|
+
newh = Bearing.absolute_degrees(@course + alpha)
|
202
|
+
twa = 180 - (720 + wad - @course - alpha) % 360
|
203
|
+
puts "Trial heading of %03dd (alpha=%d, TWA=%d)" % [newh, alpha, twa]
|
204
|
+
speed = polar.speed(Bearing.degrees_to_radians(twa))
|
205
|
+
vmg = polar.vmg(Bearing.degrees_to_radians(alpha))
|
206
|
+
#
|
207
|
+
# Adjust the speed to favour the current tack.
|
208
|
+
tack_err = twa < 0.0 ? -curr_tack : curr_tack
|
209
|
+
pvmg = vmg + vmg * tack_err / 5.0
|
210
|
+
if vmg > 1.7 and false
|
211
|
+
puts "Trial TWA: %3d, speed:%.5fkn, VMG:%.6fkn" % [twa, speed, vmg]
|
212
|
+
end
|
213
|
+
if pvmg > max_pvmg
|
214
|
+
max_pvmg = pvmg
|
215
|
+
ideal_twa = twa
|
216
|
+
ideal_vmg = vmg
|
217
|
+
end
|
218
|
+
end
|
219
|
+
#
|
220
|
+
# For the various angles, we have computed the best TWA and VMG. Now
|
221
|
+
# adjust our settings, accordingly. Don't use the poisoned VMG.
|
222
|
+
@twa = ideal_twa
|
223
|
+
@vmg = ideal_vmg
|
224
|
+
@have_new_tack = (@twa * curr_tack) < 0
|
225
|
+
puts "> Best TWA is %d, VMG is %.6fknots" % [@twa, @vmg]
|
226
|
+
#
|
227
|
+
# Check to see if we have a new heading.
|
228
|
+
newh = Bearing::absolute_degrees(wad - @twa)
|
229
|
+
puts "> HDG:%03dd, Err:%03dd" % [newh, newh - @course]
|
230
|
+
if newh != @heading
|
231
|
+
puts "New Heading! %03dd" % newh
|
232
|
+
@have_new_heading = true
|
233
|
+
@heading = newh
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
#
|
238
|
+
# Set the wind data.
|
239
|
+
def wind_data(angle, speed = 0.0)
|
240
|
+
@wind = Bearing.degrees(angle, speed)
|
241
|
+
puts "Wind:#{@wind}"
|
242
|
+
compute_heading if active?
|
243
|
+
end
|
244
|
+
|
245
|
+
#
|
246
|
+
# Wind speed (Beaufort scale)
|
247
|
+
BEAUFORT = [0, 1, 4, 7, 11, 17, 22, 28, 34, 41]
|
248
|
+
def wind_beaufort
|
249
|
+
ws = @wind.speed
|
250
|
+
end
|
251
|
+
|
252
|
+
#
|
253
|
+
# Are we at a new waypoint?
|
254
|
+
def new_waypoint?
|
255
|
+
hnw = @have_new_waypoint
|
256
|
+
@have_new_waypoint = false
|
257
|
+
active? and hnw
|
258
|
+
end
|
259
|
+
|
260
|
+
#
|
261
|
+
# Do we have a new heading?
|
262
|
+
def new_heading?
|
263
|
+
hnh = @have_new_heading
|
264
|
+
@have_new_heading = false
|
265
|
+
active? and hnh
|
266
|
+
end
|
267
|
+
|
268
|
+
#
|
269
|
+
# Do we need to tack?
|
270
|
+
def tacking?
|
271
|
+
hnt = @have_new_tack
|
272
|
+
@have_new_tack = false
|
273
|
+
active? and hnt
|
274
|
+
end
|
275
|
+
|
276
|
+
#
|
277
|
+
# Return the next (current) waypoint.
|
278
|
+
def waypoint
|
279
|
+
active? ? @waypoints[@wpt_index] : nil
|
280
|
+
end
|
281
|
+
#
|
282
|
+
# Return the mission status as a string
|
283
|
+
def status_str
|
284
|
+
mins = (@time - @start_time) * 24.0 * 60.0
|
285
|
+
hours = (mins / 60.0).to_i
|
286
|
+
days = hours / 24
|
287
|
+
mins = (mins % 60.0).to_i
|
288
|
+
hours %= 24
|
289
|
+
str = ">>>#{@time}, "
|
290
|
+
if days < 1
|
291
|
+
str += "%dh%dm" % [hours, mins]
|
292
|
+
else
|
293
|
+
str += "%dd%dh" % [days, mins]
|
294
|
+
end
|
295
|
+
str + ": My position is #{@where}"
|
296
|
+
end
|
297
|
+
|
298
|
+
#
|
299
|
+
# Name/path of mission file.
|
300
|
+
def filename
|
301
|
+
@root_dir + "/missions/%03d" % id
|
302
|
+
end
|
303
|
+
|
304
|
+
#
|
305
|
+
# Save the track.
|
306
|
+
def track_save
|
307
|
+
kml_write(File.open(track_filename, 'w'))
|
308
|
+
end
|
309
|
+
|
310
|
+
#
|
311
|
+
# The name of the track file
|
312
|
+
def track_filename
|
313
|
+
@root_dir + "/tracks/output-%03d.kml" % id
|
314
|
+
end
|
315
|
+
|
316
|
+
#
|
317
|
+
# Write the track data as a KML file.
|
318
|
+
def kml_write(file)
|
319
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
320
|
+
xml.kml('xmlns' => 'http://www.opengis.net/kml/2.2',
|
321
|
+
'xmlns:gx' => 'http://www.google.com/kml/ext/2.2') {
|
322
|
+
xml.Folder {
|
323
|
+
xml_line_style(xml, "waypointLine", "0xffcf0000", 4)
|
324
|
+
xml_line_style(xml, "repellorLine", "0xff00007f", 4)
|
325
|
+
xml_line_style(xml, "trackLine")
|
326
|
+
@waypoints.each do |wpt|
|
327
|
+
xml.Placemark {
|
328
|
+
xml.name wpt.name
|
329
|
+
xml.styleUrl "#waypointLine"
|
330
|
+
xml.LineString {
|
331
|
+
xml.extrude 1
|
332
|
+
xml.tessellate 1
|
333
|
+
xml.coordinates wpt.to_kml
|
334
|
+
}
|
335
|
+
}
|
336
|
+
xml.Placemark {
|
337
|
+
xml.styleUrl "#waypointLine"
|
338
|
+
xml.LineString {
|
339
|
+
xml.extrude 1
|
340
|
+
xml.tessellate 1
|
341
|
+
xml.coordinates wpt.to_axis_kml
|
342
|
+
}
|
343
|
+
}
|
344
|
+
end
|
345
|
+
# Requires a hack to get rid of the 'gx:' for the when tag.
|
346
|
+
file.puts builder.to_xml.gsub(/GX_/, 'gx:')
|
347
|
+
end
|
348
|
+
|
349
|
+
#
|
350
|
+
# Do a line style. The colour is of the form aabbggrr for some unknown
|
351
|
+
# reason...
|
352
|
+
def xml_line_style(xml, label, color = "0xffffffff", width = 1)
|
353
|
+
xml.Style(:id => label) {
|
354
|
+
xml.LineStyle {
|
355
|
+
xml.color color
|
356
|
+
xml.width width
|
357
|
+
xml.GX_labelVisibility 1
|
358
|
+
}
|
359
|
+
}
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
data/lib/sgs/navigate.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2013, Kalopa Research. All rights reserved. This is free
|
3
|
+
# software; you can redistribute it and/or modify it under the terms of the
|
4
|
+
# GNU General Public License as published by the Free Software Foundation;
|
5
|
+
# either version 2, or (at your option) any later version.
|
6
|
+
#
|
7
|
+
# It is distributed in the hope that it will be useful, but WITHOUT
|
8
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
9
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
10
|
+
# for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License along
|
13
|
+
# with this product; see the file COPYING. If not, write to the Free
|
14
|
+
# Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
|
15
|
+
#
|
16
|
+
# THIS SOFTWARE IS PROVIDED BY KALOPA RESEARCH "AS IS" AND ANY EXPRESS OR
|
17
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
18
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
19
|
+
# IN NO EVENT SHALL KALOPA RESEARCH BE LIABLE FOR ANY DIRECT, INDIRECT,
|
20
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
21
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
22
|
+
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
23
|
+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
24
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
25
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
26
|
+
#
|
27
|
+
|
28
|
+
##
|
29
|
+
#
|
30
|
+
module SGS
|
31
|
+
class Navigate
|
32
|
+
attr_reader :mode
|
33
|
+
|
34
|
+
MODE_SLEEP = 0
|
35
|
+
MODE_TEST = 1
|
36
|
+
MODE_MANUAL = 2
|
37
|
+
MODE_UPDOWN = 3
|
38
|
+
MODE_OLYMPIC = 4
|
39
|
+
MODE_PRE_MISSION = 5
|
40
|
+
MODE_MISSION = 6
|
41
|
+
MODE_MISSION_END = 7
|
42
|
+
MODE_MISSION_ABORT = 8
|
43
|
+
|
44
|
+
MODENAMES = [
|
45
|
+
"Sleeping...",
|
46
|
+
"Test Mode",
|
47
|
+
"Manual Steering",
|
48
|
+
"Sail Up and Down",
|
49
|
+
"Sail a Triangle",
|
50
|
+
"Pre-Mission Wait",
|
51
|
+
"On Mission",
|
52
|
+
"Mission Ended",
|
53
|
+
"** Mission Abort **"
|
54
|
+
].freeze
|
55
|
+
|
56
|
+
def initialize
|
57
|
+
@mode = MODE_SLEEP
|
58
|
+
@waypoint = nil
|
59
|
+
@curpos = nil
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# What is the mode name?
|
65
|
+
def mode_name
|
66
|
+
MODENAMES[@mode]
|
67
|
+
end
|
68
|
+
|
69
|
+
def mode=(val)
|
70
|
+
puts "SETTING NEW MODE TO #{MODENAMES[val]}"
|
71
|
+
@mode = val
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# This is the main navigator function. It does several things;
|
76
|
+
# 1. Look for the next waypoint and compute bearing and distance to it
|
77
|
+
# 2. Decide if we have reached the waypoint (and adjust accordingly)
|
78
|
+
# 3. Compute the boat heading (and adjust accordingly)
|
79
|
+
def run
|
80
|
+
puts "Navigator mode is #{mode_name}: Current Position:"
|
81
|
+
p curpos
|
82
|
+
p waypoint
|
83
|
+
case @mode
|
84
|
+
when MODE_UPDOWN
|
85
|
+
upwind_downwind_course
|
86
|
+
when MODE_OLYMPIC
|
87
|
+
olympic_course
|
88
|
+
when MODE_MISSION
|
89
|
+
mission
|
90
|
+
when MODE_MISSION_END
|
91
|
+
mission_end
|
92
|
+
when MODE_MISSION_ABORT
|
93
|
+
mission_abort
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# Navigate a course up to a windward mark which is one nautical mile
|
99
|
+
# upwind of the start position. From there, navigate downwind to the
|
100
|
+
# finish position
|
101
|
+
def upwind_downwind_course
|
102
|
+
end
|
103
|
+
|
104
|
+
#
|
105
|
+
# Navigate around an olympic triangle. Sail one nautical mile upwind of
|
106
|
+
# the current position, then sail to a point to the left-side of the
|
107
|
+
# course which is at an angle of 120 degrees to the wind. From there,
|
108
|
+
# sail back to the start position
|
109
|
+
def olympic_course
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
# Navigate the mission. This is the main "meat and potatoes" navigation.
|
114
|
+
# It concerns itself with finding the best route to the next mark and
|
115
|
+
# sailing to that
|
116
|
+
def mission
|
117
|
+
end
|
118
|
+
|
119
|
+
#
|
120
|
+
# The mission has ended - sail to the rendezvous point
|
121
|
+
def mission_end
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# The mission is aborted. Determine what to do next
|
126
|
+
def mission_abort
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# What is our current position?
|
131
|
+
def curpos
|
132
|
+
@curpos ||= SGS::GPS.load
|
133
|
+
end
|
134
|
+
|
135
|
+
#
|
136
|
+
# What is the next waypoint?
|
137
|
+
def waypoint
|
138
|
+
@waypoint ||= SGS::Waypoint.load
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/sgs/nmea.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2013, Kalopa Research. All rights reserved. This is free
|
3
|
+
# software; you can redistribute it and/or modify it under the terms of the
|
4
|
+
# GNU General Public License as published by the Free Software Foundation;
|
5
|
+
# either version 2, or (at your option) any later version.
|
6
|
+
#
|
7
|
+
# It is distributed in the hope that it will be useful, but WITHOUT
|
8
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
9
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
10
|
+
# for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License along
|
13
|
+
# with this product; see the file COPYING. If not, write to the Free
|
14
|
+
# Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
|
15
|
+
#
|
16
|
+
# THIS SOFTWARE IS PROVIDED BY KALOPA RESEARCH "AS IS" AND ANY EXPRESS OR
|
17
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
18
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
19
|
+
# IN NO EVENT SHALL KALOPA RESEARCH BE LIABLE FOR ANY DIRECT, INDIRECT,
|
20
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
21
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
22
|
+
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
23
|
+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
24
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
25
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
26
|
+
#
|
27
|
+
|
28
|
+
##
|
29
|
+
# Routines for handling sailboat navigation and route planning.
|
30
|
+
#
|
31
|
+
module SGS
|
32
|
+
class NMEA
|
33
|
+
##
|
34
|
+
# Parse and create NMEA strings for various purposes.
|
35
|
+
#
|
36
|
+
attr_accessor :args, :valid, :checksum
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
@args = Array.new
|
40
|
+
@valid = false
|
41
|
+
@checksum = 0
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Parse an NMEA string into its component parts.
|
46
|
+
def self.parse(str)
|
47
|
+
nmea = new
|
48
|
+
if nmea.parse(str) < 0
|
49
|
+
nmea = nil
|
50
|
+
end
|
51
|
+
nmea
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Parse an NMEA string into its component parts.
|
56
|
+
def parse(str)
|
57
|
+
str.chomp!
|
58
|
+
if str[0] != 36
|
59
|
+
return -1
|
60
|
+
end
|
61
|
+
str, sum = str[1..-1].split('*')
|
62
|
+
if sum.nil? or sum.to_i(16) != compute_csum(str)
|
63
|
+
return -1
|
64
|
+
end
|
65
|
+
@args = str.split(',')
|
66
|
+
@args.count
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Is the current line a GPRMC message?
|
71
|
+
def is_gprmc?
|
72
|
+
@args[0] == "GPRMC"
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# Parse a GPRMC message
|
77
|
+
# ["GPRMC", "211321.000", "A", "5309.7743", "N", "00904.5576", "W", "0.17", "78.41", "200813", "", "", "A"]
|
78
|
+
def parse_gprmc
|
79
|
+
if @args.count < 12 or @args.count > 13
|
80
|
+
return nil
|
81
|
+
end
|
82
|
+
gps = SGS::GPS.new
|
83
|
+
gps.valid = @args[2] == "A"
|
84
|
+
hh = @args[1][0..1].to_i
|
85
|
+
mm = @args[1][2..3].to_i
|
86
|
+
ss = @args[1][4..-1].to_f
|
87
|
+
us = (ss % 1.0 * 1000000)
|
88
|
+
ss = ss.to_i
|
89
|
+
dd = @args[9][0..1].to_i
|
90
|
+
mn = @args[9][2..3].to_i
|
91
|
+
yy = @args[9][4..5].to_i + 2000
|
92
|
+
gps.time = Time.gm(yy, mn, dd, hh, mm, ss, us)
|
93
|
+
gps.location = Location.parse ll_nmea(@args[3,4]), ll_nmea(@args[5,6])
|
94
|
+
gps.sog = @args[7].to_f
|
95
|
+
gps.cmg = Bearing.degrees_to_radians @args[8].to_f
|
96
|
+
gps
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Output a GPRMC message
|
101
|
+
def make_gprmc(gps)
|
102
|
+
@valid = true
|
103
|
+
@args = Array.new
|
104
|
+
@args[0] = "GPRMC"
|
105
|
+
@args[1] = gps.time.strftime("%H%M%S.") + "%03d" % (gps.time.usec / 1000)
|
106
|
+
@args[2] = 'A'
|
107
|
+
@args.concat gps.location.latitude_array
|
108
|
+
@args.concat gps.location.longitude_array("%03d%07.4f")
|
109
|
+
@args[7] = "%.2f" % gps.sog
|
110
|
+
@args[8] = "%.2f" % Bearing.radians_to_degrees(gps.cmg)
|
111
|
+
@args[9] = gps.time.strftime("%d%m%y")
|
112
|
+
@args.concat ['', '']
|
113
|
+
@args << 'A'
|
114
|
+
end
|
115
|
+
|
116
|
+
#
|
117
|
+
# Convert an array of component parts into an NMEA string.
|
118
|
+
def to_s
|
119
|
+
str = @args.join(',')
|
120
|
+
"$%s*%02X" % [str, compute_csum(str)]
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Compute an NMEA checksum
|
125
|
+
def compute_csum(str)
|
126
|
+
@checksum = 0
|
127
|
+
str.each_byte {|ch| @checksum ^= ch}
|
128
|
+
@checksum
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
#
|
133
|
+
# Convert NMEA lat/long to something useful.
|
134
|
+
def ll_nmea(args)
|
135
|
+
args[0].gsub(/(\d\d\.)/, ' \1') + args[1]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|