sgslib 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|