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/location.rb
ADDED
@@ -0,0 +1,328 @@
|
|
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 location and bearings.
|
30
|
+
#
|
31
|
+
require 'date'
|
32
|
+
require 'json'
|
33
|
+
|
34
|
+
module SGS
|
35
|
+
##
|
36
|
+
#
|
37
|
+
# Class for dealing with latitude/longitude. Includes methods for parsing,
|
38
|
+
# converting to a printable string, and so forth.
|
39
|
+
#
|
40
|
+
# Note that for convenience, we retain latitude and longitude in Radians
|
41
|
+
# rather than degrees.
|
42
|
+
#
|
43
|
+
class Location
|
44
|
+
attr_accessor :latitude, :longitude
|
45
|
+
|
46
|
+
#
|
47
|
+
# Nominal radius of the planet, in nautical miles.
|
48
|
+
# http://en.wikipedia.org/wiki/Earth_radius#Mean_radii
|
49
|
+
EARTH_RADIUS = 3440.069528437724
|
50
|
+
|
51
|
+
#
|
52
|
+
# Create the Location instance.
|
53
|
+
def initialize(lat = nil, long = nil)
|
54
|
+
@latitude = lat.to_f if lat
|
55
|
+
@longitude = long.to_f if long
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Calculate a new position from the current position
|
60
|
+
# given a bearing (angle and distance)
|
61
|
+
#
|
62
|
+
# This code was derived from formulae on the Movable Type site:
|
63
|
+
# http://www.movable-type.co.uk/scripts/latlong.html
|
64
|
+
#
|
65
|
+
# var lat2 = Math.asin( Math.sin(lat1)*Math.cos(d/R) +
|
66
|
+
# Math.cos(lat1)*Math.sin(d/R)*Math.cos(angle) );
|
67
|
+
# var lon2 = lon1 + Math.atan2(Math.sin(angle)*Math.sin(d/R)*Math.cos(lat1),
|
68
|
+
# Math.cos(d/R)-Math.sin(lat1)*Math.sin(lat2));
|
69
|
+
def calculate(bearing)
|
70
|
+
loc = Location.new
|
71
|
+
sin_angle = Math.sin(bearing.angle)
|
72
|
+
cos_angle = Math.cos(bearing.angle)
|
73
|
+
sin_dstr = Math.sin(bearing.distance / EARTH_RADIUS)
|
74
|
+
cos_dstr = Math.cos(bearing.distance / EARTH_RADIUS)
|
75
|
+
sin_lat1 = Math.sin(@latitude)
|
76
|
+
cos_lat1 = Math.cos(@latitude)
|
77
|
+
loc.latitude = Math.asin(sin_lat1*cos_dstr + cos_lat1*sin_dstr*cos_angle)
|
78
|
+
sin_lat2 = Math.sin(loc.latitude)
|
79
|
+
loc.longitude = @longitude + Math.atan2(sin_angle*sin_dstr*cos_lat1,
|
80
|
+
cos_dstr - sin_lat1*sin_lat2)
|
81
|
+
loc
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# Move to the new location
|
86
|
+
def move!(bearing)
|
87
|
+
loc = calculate(bearing)
|
88
|
+
self.latitude = loc.latitude
|
89
|
+
self.longitude = loc.longitude
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Create a new location from a string.
|
94
|
+
# Uses the instance method to parse.
|
95
|
+
def self.parse_str(str)
|
96
|
+
loc = new
|
97
|
+
loc.parse_str(str)
|
98
|
+
loc
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Create a new location from a lat/long string pair
|
103
|
+
# Uses the instance method to parse.
|
104
|
+
def self.parse(latstr, longstr)
|
105
|
+
loc = new
|
106
|
+
loc.parse(latstr, longstr)
|
107
|
+
loc
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Parse a lat/long value (in degrees)
|
112
|
+
def parse_str(str)
|
113
|
+
latstr, longstr = str.split(',')
|
114
|
+
parse(latstr, longstr)
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Parse a lat/long value pair (in degrees)
|
119
|
+
def parse(latstr, longstr)
|
120
|
+
@latitude = ll_parse(latstr.split, "NS")
|
121
|
+
@longitude = ll_parse(longstr.split, "EW")
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# Is this location valid?
|
126
|
+
def valid?
|
127
|
+
@latitude and @longitude
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# Display the lat/long as a useful string (in degrees).
|
132
|
+
def to_s
|
133
|
+
if valid?
|
134
|
+
"%s, %s" % [ll_to_s(@latitude, "NS"), ll_to_s(@longitude, "EW")]
|
135
|
+
else
|
136
|
+
"unknown"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
#
|
141
|
+
# Display the lat/long as it would appear in a KML file.
|
142
|
+
def to_kml(sep = ' ')
|
143
|
+
vals = [@longitude, @latitude, 0.0]
|
144
|
+
str_vals = vals.map {|val| "%.6f" % Bearing.radians_to_degrees(val)}
|
145
|
+
str_vals.join(sep)
|
146
|
+
end
|
147
|
+
|
148
|
+
#
|
149
|
+
# Helper functions for working in degrees.
|
150
|
+
def latitude_degrees
|
151
|
+
Bearing.radians_to_degrees @latitude
|
152
|
+
end
|
153
|
+
|
154
|
+
def latitude_degrees=(val)
|
155
|
+
@latitude = Bearing.degrees_to_radians val
|
156
|
+
end
|
157
|
+
|
158
|
+
def latitude_array(fmt = nil)
|
159
|
+
make_ll_array latitude_degrees, "NS", fmt
|
160
|
+
end
|
161
|
+
|
162
|
+
def longitude_degrees
|
163
|
+
Bearing.radians_to_degrees @longitude
|
164
|
+
end
|
165
|
+
|
166
|
+
def longitude_degrees=(val)
|
167
|
+
@longitude = Bearing.degrees_to_radians val
|
168
|
+
end
|
169
|
+
|
170
|
+
def longitude_array(fmt = nil)
|
171
|
+
make_ll_array longitude_degrees, "EW", fmt
|
172
|
+
end
|
173
|
+
|
174
|
+
#
|
175
|
+
# Subtract one location from another, returning a bearing
|
176
|
+
def -(loc)
|
177
|
+
Bearing.calculate(self, loc)
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
#
|
182
|
+
# Parse a string into a lat or long.
|
183
|
+
def ll_parse(args, nsew)
|
184
|
+
dir = args[-1].gsub(/[\d\. ]+/, '').upcase
|
185
|
+
args.map! {|val| val.to_f}
|
186
|
+
val = args.shift
|
187
|
+
val = val + args.shift / 60.0 if args.length > 0
|
188
|
+
val = val + args.shift / 3600.0 if args.length > 0
|
189
|
+
Bearing.degrees_to_radians val * ((nsew.index(dir) == 1) ? -1 : 1)
|
190
|
+
end
|
191
|
+
|
192
|
+
#
|
193
|
+
#
|
194
|
+
def ll_to_s(val, str)
|
195
|
+
if val < 0.0
|
196
|
+
chr = str[1]
|
197
|
+
val = -val
|
198
|
+
else
|
199
|
+
chr = str[0]
|
200
|
+
end
|
201
|
+
"%8.6f%c" % [Bearing.radians_to_degrees(val), chr]
|
202
|
+
end
|
203
|
+
|
204
|
+
#
|
205
|
+
# Create a Lat/Long array suitable for an NMEA output
|
206
|
+
def make_ll_array(val, nsew, fmt = nil)
|
207
|
+
fmt ||= "%02d%07.4f"
|
208
|
+
if (val < 0)
|
209
|
+
val = -val
|
210
|
+
ne = nsew[1]
|
211
|
+
else
|
212
|
+
ne = nsew[0]
|
213
|
+
end
|
214
|
+
deg = val.to_i
|
215
|
+
val = (val - deg) * 60
|
216
|
+
[fmt % [deg, val], ne.chr]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
##
|
221
|
+
# Class for dealing with the angle/distance vector.
|
222
|
+
#
|
223
|
+
# Note that for convenience, we retain the angle in Radians. The
|
224
|
+
# distance is in nautical miles.
|
225
|
+
class Bearing
|
226
|
+
attr_accessor :distance
|
227
|
+
|
228
|
+
#
|
229
|
+
# Create the Bearing instance.
|
230
|
+
def initialize(angle = 0.0, distance = 0.0)
|
231
|
+
self.angle = angle.to_f
|
232
|
+
self.distance = distance.to_f
|
233
|
+
end
|
234
|
+
|
235
|
+
#
|
236
|
+
# Create a bearing from an angle in degrees.
|
237
|
+
def self.degrees(angle, distance)
|
238
|
+
new(Bearing.degrees_to_radians(angle), distance)
|
239
|
+
end
|
240
|
+
|
241
|
+
#
|
242
|
+
# Handy function to translate degrees to radians
|
243
|
+
def self.degrees_to_radians(deg)
|
244
|
+
deg.to_f * Math::PI / 180.0
|
245
|
+
end
|
246
|
+
|
247
|
+
#
|
248
|
+
# Handy function to translate radians to degrees
|
249
|
+
def self.radians_to_degrees(rad)
|
250
|
+
rad.to_f * 180.0 / Math::PI
|
251
|
+
end
|
252
|
+
|
253
|
+
#
|
254
|
+
# Handy function to re-adjust an angle away from negative
|
255
|
+
def self.absolute(angle)
|
256
|
+
(angle + 2.0 * Math::PI) % (2.0 * Math::PI)
|
257
|
+
end
|
258
|
+
|
259
|
+
#
|
260
|
+
# Another handy function to re-adjust an angle (in degrees) away from
|
261
|
+
# negative.
|
262
|
+
def self.absolute_degrees(angle)
|
263
|
+
(angle + 360) % 360
|
264
|
+
end
|
265
|
+
|
266
|
+
#
|
267
|
+
# Haversine formula for calculating distance and angle, given two
|
268
|
+
# locations.
|
269
|
+
#
|
270
|
+
# To calculate an angle and distance from two positions:
|
271
|
+
#
|
272
|
+
# This code was derived from formulae on the Movable Type site:
|
273
|
+
# http://www.movable-type.co.uk/scripts/latlong.html
|
274
|
+
#
|
275
|
+
# var d = Math.acos(Math.sin(lat1)*Math.sin(lat2) +
|
276
|
+
# Math.cos(lat1)*Math.cos(lat2) *
|
277
|
+
# Math.cos(lon2-lon1)) * R;
|
278
|
+
# var y = Math.sin(dLon) * Math.cos(lat2);
|
279
|
+
# var x = Math.cos(lat1)*Math.sin(lat2) -
|
280
|
+
# Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
|
281
|
+
# var angle = Math.atan2(y, x).toDeg();
|
282
|
+
def self.calculate(loc1, loc2)
|
283
|
+
bearing = new
|
284
|
+
sin_lat1 = Math.sin(loc1.latitude)
|
285
|
+
sin_lat2 = Math.sin(loc2.latitude)
|
286
|
+
cos_lat1 = Math.cos(loc1.latitude)
|
287
|
+
cos_lat2 = Math.cos(loc2.latitude)
|
288
|
+
sin_dlon = Math.sin(loc2.longitude - loc1.longitude)
|
289
|
+
cos_dlon = Math.cos(loc2.longitude - loc1.longitude)
|
290
|
+
bearing.distance = Math.acos(sin_lat1*sin_lat2 + cos_lat1*cos_lat2*cos_dlon) *
|
291
|
+
Location::EARTH_RADIUS
|
292
|
+
y = sin_dlon * cos_lat2
|
293
|
+
x = cos_lat1 * sin_lat2 - sin_lat1 * cos_lat2 * cos_dlon
|
294
|
+
bearing.angle = Math.atan2(y, x)
|
295
|
+
bearing
|
296
|
+
end
|
297
|
+
|
298
|
+
#
|
299
|
+
# Set the angle
|
300
|
+
def angle=(angle)
|
301
|
+
@angle = Bearing.absolute(angle)
|
302
|
+
end
|
303
|
+
|
304
|
+
#
|
305
|
+
# Get the angle
|
306
|
+
def angle
|
307
|
+
@angle
|
308
|
+
end
|
309
|
+
|
310
|
+
#
|
311
|
+
# Return the angle (in degrees)
|
312
|
+
def angle_degrees
|
313
|
+
Bearing.radians_to_degrees(@angle).to_i
|
314
|
+
end
|
315
|
+
|
316
|
+
#
|
317
|
+
# Get the back-angle (the angle viewed from the opposite end of the line)
|
318
|
+
def back_angle
|
319
|
+
Bearing.absolute(@angle - Math::PI)
|
320
|
+
end
|
321
|
+
|
322
|
+
#
|
323
|
+
# Convert to a string
|
324
|
+
def to_s
|
325
|
+
"BRNG %03dd,%.3fnm" % [angle_degrees, @distance]
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
data/lib/sgs/logger.rb
ADDED
@@ -0,0 +1,96 @@
|
|
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 logging.
|
30
|
+
#
|
31
|
+
require 'date'
|
32
|
+
|
33
|
+
module SGS
|
34
|
+
#
|
35
|
+
# Waypoint, Attractor, and Repellor definitions
|
36
|
+
class Logger < RedisBase
|
37
|
+
attr_accessor :watch
|
38
|
+
|
39
|
+
#
|
40
|
+
# Watch names and definitions. The first watch is from 8PM until
|
41
|
+
# midnight (local time), the middle watch is from midnight until
|
42
|
+
# 4AM. The morning watch is from 4AM until 8AM. The forenoon watch
|
43
|
+
# runs from 8AM until noon and the afternoon watch runs from noon
|
44
|
+
# until 4PM. The dog watches run from 4PM until 8PM and around we
|
45
|
+
# go again.
|
46
|
+
#
|
47
|
+
# Logs are sent back to base every four hours (6 per day) starting
|
48
|
+
# at midnight UTC. However, some of the reporting is based on
|
49
|
+
# the watch system rather than UTC. For example, reporting battery
|
50
|
+
# voltage is most useful at the start of the forenoon watch, because
|
51
|
+
# this represents the lowest voltage point after driving the boat all
|
52
|
+
# night. As a result, the watch ID is computed based on the longitude,
|
53
|
+
# which is a rough approximation of the timezone.
|
54
|
+
FIRST_WATCH = 0
|
55
|
+
MIDDLE_WATCH = 1
|
56
|
+
MORNING_WATCH = 2
|
57
|
+
FORENOON_WATCH = 3
|
58
|
+
AFTERNOON_WATCH = 4
|
59
|
+
DOG_WATCH = 5
|
60
|
+
ALARM_REPORT = 7
|
61
|
+
|
62
|
+
WATCH_NAMES = [
|
63
|
+
"First Watch", "Middle Watch", "Morning Watch",
|
64
|
+
"Forenoon Watch", "Afternoon Watch", "Dog Watch",
|
65
|
+
"", "** ALARM REPORT **"
|
66
|
+
].freeze
|
67
|
+
|
68
|
+
def initialize()
|
69
|
+
@watch = FIRST_WATCH
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# Convert the watch ID to a name
|
74
|
+
def watch_name
|
75
|
+
WATCH_NAMES[@watch]
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Determine the watch report. This takes account our actual latitude
|
80
|
+
# and the current time. It does a rudimentary timezone conversion and
|
81
|
+
# calculates the watch. Note that what we're calculating here is what
|
82
|
+
# was the previous watch. So any time after 23:45 (local), we'll report
|
83
|
+
# from the First Watch. Right up until 03:45 (local) when we'll start
|
84
|
+
# reporting from the Middle Watch.
|
85
|
+
def determine_watch(longitude)
|
86
|
+
utc = Time.now
|
87
|
+
local_hour = utc.hour + longitude * 12.0 / Math::PI
|
88
|
+
p utc
|
89
|
+
p local_hour
|
90
|
+
local_hour += 24.0 if local_hour < 0.0
|
91
|
+
local_hour -= 24.0 if local_hour >= 24.0
|
92
|
+
@watch = ((local_hour / 4.0) + 0.25).to_i
|
93
|
+
@watch = FIRST_WATCH if @watch == 6
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|