sgslib 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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